Introduction to LPC4

Written by Fredrik Hubinette
LPC is a dynamic, MUD-oriented language with a syntax that looks like C. The mode of programming is similar to structured BASIC or byte-compiled LISP in that all data is dynamically allocated, and that a variable can hold any value not just what it was declared as. It is not a compiling language, merely byte-compileing and the code is then interpreted by a program written in C.

The compiler and interpreter are integrated in a program called driver, it is capable of dealing having any number of programs in memory, but it is not multitasking, thus only one program can run at a time.

A program is a set of functions and variable declaration read from a LPC-source file, there is no intermediate form like 'executables' or dot-o files. From these programs, objects are cloned. Every object has it's own memory for the global variables in the program. It is not possible to call a function without an object to call it in.

All values in LPC consists of a cell contaning both the type of the cell, and the data itself. The data might be an integer, float or a pointer to some other data. In C only the compiler knows the type of a variable, it is not possible to know if a variable is an integer or float by looking at it's value afterwards.

Now let's look at an example program:

	string gazonk;

	void main(int argc,string *argv)
	  int e,foo,bar;
Ok, everything looks like C, except that write() statement, which is a predefined LPC function. Documentation of all such function can be found in the directory doc/efun. If you aren't familiar with C, then maybe some explainations might be in order:

The first line 'string gazonk;' is a declaration of a global variable of the type 'string'. Then follows a function that is declared to return void, which means that it don't return anything and takes an int called argc and an array of strings called argv as argument. Then the line 'int e,foo,bar;' reserves space for 3 local varables of the type int. Then follows a loop that first sets e to 0, and runs while e<10 and writes e and increases e for every loop. See doc/lpc/control to find out more about loops etc.

Let's assume this program was saved in the file test.c and that we want to load and we want to make a program that loads it and calls main(). Then this is what you would write in another program:

	void do_it()
	  object o;
This example is somewhat simplifyed, normally a full path name is needed as an arument to clone_object. Note that o will be a pointer to the object cloned, not the object itself. The operator -> returns a function pointer to the function main in the object pointed to by o. The paratheses are then the operator that calls the function that is written just before it. Here I give main() the arguments 1 and an array with the string as the only element. Because of LPC's dynamic structure, the array is not really a constant, instead a structure in memory is built at execution time and a pointer to it is sent to main(). It is then freed when it no longer needed.

All lpc types have the properties of ether a value or a pointer, ints, strings and floats all works like values, while arrays, mappings, lists, objectpointer and functionpointers all behave like pointers. The main difference is that values are copied when an assignment is made, while in the case of a pointer anoter reference is made to the object pointed to. Thus if a is an array and b is assigned to a like: b=a; then any destructive change in the array a will also be seen in the array b because they will be pointers to the same array.

A chain of calls always starts from with a call from inside the driver, all functions that has special meanings to the driver are described in the directory doc/lfun. Let's look a little closer at the LPC syntax:

[] will mean that that part is optional unless otherwise is explained.
<> will mean that that part should be filled in by some text unless otherwise is explained.
... means that the previous list can be of any number of items, even zero.
A global variable definition
<type> <name> [=<statement>] , <name> [=<statement>] , .... ;
This defines global variables and optionally sets their values. If they are not initiated they will be set to an integer zero.

A function declaration looks like this
<type> <name> ( <type> <arg_name>, <type> <arg_name> , ... ) ;
The fist type is the return type, then follows the name of the funcion. The second type is the type of the first argument, which name follows. Function declarations are used to let the compiler make an function header that can be used before the function is defined. This is called a 'forward declaration' in PASCAL.

A function definition
<type> <name> ( <type> <arg_name>, <type> <arg_name> , ... ) <block>
The first line corresponds to a function declaration, that is exacly what it is, then follows the 'body' of the function, which is a program block with the argument for the funcion defined as local variables.

A block
{ <local declarations> <statements> }
The local declarations is just a list of variable declarations of the same form as the global declarations but the variables are only usable within the block.

A statement
A statement can be many things, it can be a for-loop, a function-call an if-else statement, a switch and assignment or simply a block. Basically can be said that a statement has two forms: one containing another block, like if-else statements, switches, for-loops etc. and one that is an expression followed by a semicolon.

Here is a list of operators that can be used in expressions, listed in ascending priority order:

<> and [] are literal in this listing.
A,B,C... are any expression
a,b,c... are variables

Priority 0:
A?B:C returns B if a is nonzero and C otherwise
Priority 1
a=B assigns the value of b to the variable a and returns that value
a+=B same as a=a+(B)
a-=B same as a=a-(B)
a/=B same as a=a/(B)
a*=B same as a=a*(B)
a&=B same as a=a&(B)
a|=B same as a=a|(B)
a^=B same as a=a^(B)
a&&=B same as a=a&&(B)
a>>=B same as a=a<<(B)
a<<=B same as a=a>>(B)
Priority 2
A||B returns A if A is nonzero otherwise B
A&&B returns A if A is zero otherwise B
Priority 3
A|B returns A or B bitwise ored
A&B returns A and B bitwise anded
A^B returns A xor B

Priority 4
A==B returns 1 if A is equal to B otherwise 0
A!=B returns 1 if A isn't equal to B otherwise 0
A<B return 1 if A is lesser than B otherwise 0
A>B return 1 if A is greater than B otherwise 0
A<=B return 1 if A is lesser than or equal to B otherwise 0
A>=B return 1 if A is greater than or equal to B otherwise 0

Priority 5
A<<B returns A bitwise shifter B positions right
A>>B returns A bitwise shifter B positions left

Priority 6
A+B returns A plus B
A-B returns A minus B

Priority 7
A*B returns A multiplyed with B
A/B returns A divided by B

Priority 8
!A returns 1 if A is zero, 0 otherwise
~A returns the bitwise compliment to A
(type)A returns a casted to the type inside the parentheses
a++ increases the variable a (which must be an integer) with 1 and returns the value a had _before_ it was increased
++a increases the variable a with 1 and returns the value of a
a-- decreases the variable a (which must be an integer) with 1 and returns the value a had _before_ it was decreased
--a increases the variable a with 1 and returns the value of a
A[B] return the index B in the array or mapping A (this is also a variable)

Priority 9
A[B..C] return all elements in A from B to C
A(B) calls the function A with the argument b
A->foo returns the function called foo in the object A
({A,B}) returns an array with A and B as elements
([A:B]) returns A mapping with A as an index to the data B
(<A,B>) returns A list with A and B as memebers

1 2 3 4 ... returns an integer
"foobar" returns the string foobar
1.0 2.2 3.14 ... returns a float

foobar(A,B) or efun::foobar(A,B) call the efun called foobar with A and B as arguments.
(A) returns A

Note that the order of evaluation within prioritys normally is left to right, but not in all cases. See Operators for further details.

A program may 'inherit' one or more programs, this is simply a copy operation that copies all the functions and variable defenitions to the new program, it does however have a lot of advantages over the include-statement, which merely includes text into the new program: It saves memory, because the body of the functions is never copied, the original is used instead. It saves time, because the inherited code doesn't have to be compiled again. To inherit a program simply write 'inherit <filename>' in the beginning of the file. Note that defines and macros will not be inherited, though.

LPC4 vs LPC3 - For thoose of you who know some LPC3

A lot of changes have been made in LPC4 compared to earlear versions I will try to describe what I have done, and why here.
Obviously floats can be very nice, so I added them as a new type.

A list is really a somewhat ill-named type. It is _not_ a type for linked list. It is instead a type of array in which the order of the members is not important. Like a mapping without data. It has been made so that lookups, and logical operations such as anding and oring is quite fast.

Functionpointers is now used for _all_ LPC function calls (not for efuns though) The name of a defined function is a 'constant functionpointer and parentheses are used to call it. Other ways of writing functionpointers include: file::fun, ::fun, obj->fun and lambdafunctions. A funcitonpointer consists of an objectpointer and an index that tells which function is pointed to in that object.

Is essentially a nice way of defining functions inside another function for instance:
	  return lambda() { return 2; }
Will return a functionpointer to a function that will return 2 when called.

No 'master' objects, only clones
I never liked the need for a 'master' for the clones so I removed the cloning of a masterobject and made the functions load() and update() to handle programs. I also invented a way to 'label' clones so that it would still be possible to address objects not yet loaded.

Object labels:
When a string is casted to an objectpointer, this will happen: If a # is present in the string, the part before it will be called file and the part after a label. If there is no # in the string or the label is an empty string, then only the file will be considered. It the program residing in the file file isn't already loaded, it will be loaded and a clone will be made. This clone will be labeled with the original string (without the # if the label was empty) and the label will be sent to create() as an argument. The object may then selfdestruct if it didn't like it's label, otherwise that object will be returned and hashed with that label. Next time that string is casted to an object, this clone will simply be digged out of the hashtable.

There is a similar function in clone_object(), with the only difference that it doesn't actually label the object, it just sends the label to create()

A lot of new efuns have been added (of course)
read about them in doc/efun

The compiler has been rewritten
It now uses parse-trees and optimizes the code somewhat.

Casting has been changed, casting to most types now actually does something. A list of actions during casting follows:
  1. string to int: atoi()
  2. string to float: atod()
  3. string to object: loads an object as described above
  4. string to function: is equal to this_object()->string

No casting
Previously some implicit casting were done from strings to objects at runtime, now the compiler tries to detect where such casting is needed and insers castings there. However, if the types doesn't match exactly (ie. if one of them is 'mixed') no casting implicit casting will be done automatically.

The old communication code that used interactive objects has been replaced by general socket efuns. The removal of the interactive structure also led to the removal of this_interactive(), find_player() ed(), query_idle(), snoop(), trace(), traceprefix() and some other gunk in the driver.

has been removed, they only complicated things.

Binary strings
Some functions can now handle binary strings, that is strings that contains zeros.