NOTE:
This file is a work in progress, so when in doubt look at the source code.
It should give you the general idea, but it still needs a lot of love.
The (DGD)LPC Reference Manual
The LPC Reference Manual
Copyright © 1995 - 1997 Dworkin B.V. This document has been released
into the public domain.
1. Introduction
1.1. Purpose
This document is a formal description of the LPC programming language.
The LPC programming language is derived from C and named after its primary
creator, Lars Pensjö. Several dialects of LPC exist; the programming
language described here is the one used in Dworkin's Generic Driver
(DGD) version 1.1.
1.2. History
2. Environment
3. Language
In the syntax notation of this document, syntactic categories are
indicated by italic type.
3.1. Lexical elements
3.1.1. Tokens
A token is the minimal lexical element of the language. The
categories of tokens are:
identifiers,
keywords,
constants,
operators and
punctuators. Tokens are separated by
white space: blanks, horizontal and vertical tabs, newlines, carriage returns,
form feeds and comments.
3.1.2. Comments
A comment is a sequence of characters starting with the characters /*
and terminated by the characters */, or a sequence of characters that
starts with // and terminated by a new line. All characters in between are
part of the comment. Comments count as whitespace separating tokens, and
may not be nested. The sequence /* does not start a comment if it
is part of a string. Note: hydra does not support // style comments currently.
Examples:
// This is a comment
/* This is another
comment */
An identifier is an arbitrary long sequence of letters, digits and
underscores. The first character must be a letter or underscore. Upper- and
lower-case letters are different. Only the first 1023 characters of
identifiers are significant.
The following identifiers are reserved for use as keywords:
atomic |
break |
case |
catch |
continue |
default |
do |
else |
float |
for |
function* |
goto* |
if |
inherit |
int |
mapping |
mixed |
nil |
nomask |
object |
private |
return |
rlimits |
static |
string |
switch |
varargs |
void |
while |
Note: dgd does not support goto currently.
Note: function is only available if compiled with -DCLOSURES
A constant is either an
integer constant,
floating constant,
string constant or
character constant.
An integer constant is a decimal constant, octal constant
or hexadecimal constant.
A sequence of digits is taken to be a decimal constant (base ten)
unless it begins with 0. A sequence of digits starting with 0
and not including 8 or 9 is an octal constant
(base 8). A sequence of digits and upper- or lower-case letters in range
A through F, preceded by 0x or 0X is a
hexadecimal constant (base 16, where A through F
have the decimal values 10 through 15).
Integer constants are represented by 32 bit 2's complement arithmetic,
and range from -2147483648 through 2147483647. It is an error to specify a
decimal constant that is too large to be represented. Octal
constants and hexadecimal constants, when too large, are
truncated to the lower 32 bits.
A floating constant consists of an integer part, a decimal point, a
fraction part, and an exponent part, consisting of an e or E
and an optionally signed integer exponent. Either the integer part or the
fraction part, but not both, may be missing. Either the decimal point and
fraction part, or the exponent part, but not both, may be missing.
Floating constants represent a floating point number in the range of
-1.79769313485E308 through 1.79769313485E308, the smallest possible number
being -2.22507385851E-308. It is an error to specify a floating constant
that is too large to be represented. Numbers which are too small are
flushed to zero.
A string constant is a sequence of zero or more characters enclosed
in double quotes. All characters, with the exception of newline, can be used in
a string constant. A backslash character \ introduces an
escape sequence, which consists of at least 2 and at most 5 characters
(including the backslash), and which is translated into a single character in
the string. The following escape sequences can be used:
\a = 007 (bell) \o
\b = 010 (backspace) \oo
\f = 014 (form feed) \ooo
\n = 012 (newline) \xh
\r = 015 (carriage return) \xhh
\t = 011 (horizontal tab) \xhhh
\v = 013 (vertical tab) \c
The value of \a, \b, \f, \n, \r,
\t and \v is the octal value shown. \o,
\oo and \ooo constitute an integer of at most 3 octal
digits, the octal value of which specifies a single character. \xh,
\xhh and \xhhh constitute an integer of at most 3 hexadecimal
digits, the hexadecimal value of which specifies a single character. All other
escape sequences \c specify the character c (any character
except a, b, f, n, r, t,
v, x, or a digit), which itself is not interpreted in any
special way.
A character constant consists of a single character or escape sequence,
enclosed in single quotes. All characters except newline can be used.
Escape sequences are handled as with string
constants.
The following are operators:
[ ] ( ) ->
++ -- + - ~ ! ... catch
* / % << >> < > <= >= == != & ^ | && ||
? :
= *= /= %= += -= <<= >>= &= ^= |=
,
Punctuators are like a '.' in a sentence, there are a variety of punctuators
but they are only used in key places. For example when your defining a
variable, you might type: int x, y;. The ';' is a punctuator that
says this is the end of this statement. The ',' says I have another int
that I want to define. The following are some of the punctuators you will see:
, ; :
We'll introduce them as we need them, because they need context to make sense.
3.2. Expressions
3.3. Constant expressions
3.4. Declarations
A Declaration can be a
variable declaration,
array declaration,
function declaration.
A variable declaration has the following form:
TYPE Identifier;
If you have multiple variables of the same time, you can separate them by a
coma to keep them on the same line: TYPE Identifier1, Identifier2;
Examples:
int x;
mixed bing;
float a, b;
The following types are supported:
Types can also have Type Modifiers
which will change how the variable or function works.
Nil is a special value, its not actually a type but it is the value
of a uninialized variable. So you can see if a variable is undefined by
seeing if it is equal to nil. Example:
/* Note this does not work for type int, but does for other types: string, mapping, etc... */
string x;
if (x == nil) write("You need to set x");
Declares the variable to be an integer, for a function it says the
function will return an integer. See
integer constant for valid integers.
Declares the variable to be a float, for a function it says the
function will return a float. See
floating constant for valid floats.
Declares the variable to be a string, for a function it says the
function will return a string. See
string constant for valid strings.
Declares the variable to be an object. The driver deals with objects as a
basic type. If you create a room, a monster, a player, or a sword, all of
these are considered objects to the driver. For a function it says the
function will return an object.
A mapping is similar to an array,
execpt you get to name your indices.
It is also sometimes called an associative array or a hash.
Think of it as a key/value pair. Example:
mapping location;
location['fred'] = 'home';
write("Fred is " + location['fred'] + ".\n");
The mixed type is used when a function will take multiple types, you can
use the typeof function to find out what type the variable is currently
using. A good example of using a mixed type would be if a function
returns a string or an int depending on what gets passed in. You want to
avoid using the mixed type to just get around type checking, limit it to
cases where its truely useful to have multiple types.
The void type is only valid for functions. It says that the function
does not return anything. You can also use void to say that a function has
no paramaeters passed into it. Examples:
void do_stuff(int x) { write("I did stuff " + x + " times.\n"); }
int do_stuff2(void) { write("I did stuff.\n"); }
Type modifiers appear before a type and provide special instructions
to the driver for how handle the special type. A type can have multiple
modifiers associated with it.
There are the following type modifiers:
private,
static,
nomask,
atomic
The private type modifier is valid for both functions and variables.
When a variable or fuction is marked as private, it becomes only accessable
by the object in which it is defined. Not even child objects that inherit
this object will be able to access them. You can use private functions
and variables to insure that others access your data the way
you want them to access it.
The static type modifier behaves differently for variables and functions.
For variables that are declared as static, the variable will be defined
globally for that object, and it will not be saved when save_object is called.
For functions that are declared as static, the function will only be available
to the object and it's children.
The nomask type modifier is only valid for functions, it
makes it so that you can not redefine a function. To put it another way,
a child object can not have a function with the same name, it will use
the parents version of the function.
The atomic type modifier is only valid for functions, it
makes it so that if an error occurs durring the function, the
driver will roll everything back to before the function was called.
It's a saftey net of sorts. If its modifying an object and half way
through it has a problem, the object will get reset to its state
before the function call. Atomic functions have a couple of drawbacks.
They are forbidden from working with files in any way(renaming, writing,
reading, etc...). They also have some overhead, they take twice as many
ticks as they would if they were not declared atomic.
An array is an extension of a basic type in lpc, which turns it into an
ordered list. You can denote an array in two ways:
basic_type *Identifier; or basic_type Identifier[NUMBER];
In the first your saying you want an array of basic_type but your not sure
how big it is. In the second, your saying you want the array to have NUMBER
elements in it. When indexing an element in an array you start at 0. So
if you declare an array 'myarray' to have 5 elements, they would be
addressed like this:
myarray[0], myarray[1], myarray[2], myarray[3], myarray[4]
Examples:
int x[5];
string *strings;
A function declaration can be a
function prototype, or a
function definition.
A prototype is used to reserve a name for a function, and specify the
number of parameters you need to give it as well as define what the function
will return, without defining the actual function. It allows the driver
to put the function in the symbol table even though it does not know what the
function is yet. It is basically saying "hey I'm going to make a function
that looks like this later".
A function prototype has the form: type Identifier(paramaters);
A function definition has the form: type Identifier(paramaters) { statements }
3.5. Statements
3.6. Inheritance
Inheritance is one of the key building blocks of lpc. The mudlib defines
some standard objects. For example, maybe your mudlib has:
/std/object.c, /std/room.c or /std/monster.c.
You can use these building blocks by inheriting them. Inherit statements
must come before any other code, including #include preprocessor statements.
The inherit syntax can have the following syntax:
[priviate] inherit "FILENAME";
[priviate] inierit Identifier "FILENAME";
You can use an Identifer if your inheriting from multiple objects and want to
explicitly call functions from one of them. The private option will make it
so that only this object will have access to the variables and functions
inherited by the inherit statement. You will not be able to access them
from a call_other function, or if you inherit this object, you will not
have access to the privately inherited stuff.
Examples:
inherit "/std/room.c";
void create( void ) {
::create();
set_short("The void");
set_long("A vast nothingness.");
}
or you can also do something like this:
inherit room "/std/room.c";
inherit ob "/std/object.c";
void create( void ) {
room::create();
ob::create();
set_short("The void");
set_long("A vast nothingness.");
}
Because we have inherited our standard room, we get all of the code
in our room file for free. Because our room code has a function for
set_long, we do not need to define set_long, we can just call it.
This makes programming in lpc much more readable and maintainable.
If we want to we can override our set_long function by defining a new
function with the same name. In the above example, we have wrote
over the create function. With a new function that sets a short and
long description for our room. the :: operator is used to call a function
that is defined at a higher level. So ::create(); says first call
the create function that was defined before this create function.
So its important to call ::create(); so that we do not have to
duplicate all of the code in /std/room.c's create function.
We can just call ::create(); and then after that make any additional
modifications we want to make. If we don't care about the previously
defined create function we can remove the ::create(); call.
3.7. Preprocessing directives
Preprocessor commands start with a #, these commands, get run before compile
time. They are used to optionally compile bits of code and or to instruct
the compiler how to do something. Many times they are used to make things
more portable. The following preprocessor directives are recognized:
include,
define,
undef,
ifdef,
ifndef,
if,
endif,
else,
elif,
pragma,
line,
error
You use #include to include a header in a file. A header file in general
usually has special definitions and function prototypes. For example if you
were writing a NETWORK header, you might have a definition for a
default network port, and function prototypes for connect, disconnect etc..
Header files are kind of like an API for the software you write.
They make it simple to change important settings and explain how you use
a library. In general they should not include code in the header file.
The syntax for an include looks like this:
#include <file.h>
#include "filename.h"
In general, the first case is for a standard header in the default search path
and ""'s are used to include your own header. It starts by searching the
current directory.
Examples:
#include <std.h>
#include "room.h"
You use #define lots of times in a header file (filename.h)
to make it easy to change certain values. The syntax for
a define looks like this:
#define Identifier VALUE
#define Identifier
If VALUE is omitted, it defines the Identifier but your not sure
what the value is. It may be nil, or it may be 1, or it may be something
else depending on implementation of the driver.
Examples:
#define MAX_STR 255
#define DICTIONARY_SIZE 2000
#define DEFAULT_PORT 4000
#define DEBUGGING_ON
You use #undef to undefine a previously defined item. The syntax
for undef is:
#undef Identifier
Examples:
#undef MAX_STR
#undef DEFAULT_PORT
Ifdef is used to instruct the driver to optionally evaluate some code.
While we are explaining ifdef we also need to introduce #endif since it
is needed to complete an ifdef statement.
#ifdef has the following syntax:
#ifdef Identifier
(insert your code here)
#endif
So if whatever Identifier you have specified is defined to some value
(with an #define, or it can also be passed to the driver at runtime)
Then it will evaluate the code until it hits a #endif If there is no
#endif the driver will throw an error. Note: #endif is used in multiple
places to designate then end of a block of code, its sometimes hard to keep
track of it all so its good to add a comment saying this #endif goes with
the Identifier you specify. Note, ifdef does not actually check the
value of your Identifier, it just checks to see if it is defined.
Something can be defined as 0 for example and ifdef will still say
yes its defined. If you want to check the value of something that is
defined, you will want to use a more complicated #if statement, which
you can find more info on later in the document.
Example:
#ifdef DEBUG
write("Were in debugging mode.\n");
#endif // DEBUG
Ifndef works the same way as ifdef but it only gets called if the Identifier
you specify is not defined.
Example:
#ifndef DEBUG
#define DEBUG 1
#endif
An if statement is used for more complicated tests, you can test
the value of a defined item, or for variations etc... It works
similar to the if conditional statement but at the preprocessor level.
Examples:
#if defined(DEBUG) || defined(VERBOSE)
write("Welcome to VERBOSE MODE!\n");
#endif
#if (3 == DEBUG)
write("Were in really verbose MODE.\n");
#elif (2 == DEBUG)
write("Were in verbose MODE.\n");
#elif (1 == DEBUG)
write("were in semi verbose MODE.\n");
#else
write("Were in quiet MODE.\n");
#endif
You use #pragma to pass a flag to the driver. In general
This is used for platform specific code. DGD currently ignores
#pragma statements. The syntax for Pragma is:
#pragma STRING
Note: the STRING is determined by the DRIVER its not something you can
just make up. If the DRIVER does not recognize a STRING it will just ignore
it and or maybe send a warning.
Used to override the driver's automatic detection of line# and or filename.
Currently the code says line# is always ignored. I think this is suppose
to be used to get around bugs, you probably want to avoid this unless
you find a situation where you really need it. If your familiar with
FLEX, BISON and or YACC, you know that sometimes your code is generated
on the fly and you could use #line to tell people the real file and
position in the file they are interested in looking at for debugging purposes,
instead of the generated code which only exists for a short time.
(If you don't know what I'm explaining, you probably do not need to
worry about it.)
Examples:
#line 22 "myfile.c"
#line "myfile.c"
#line 22
Call an error message, This is usually based on some condition that is
external to the #error command,
Example:
#ifdef APSTUDIO_INVOKED
#error this file is not editable by Microsoft Visual C++
#endif