LensC

Quick Links



Overview

Lensc is a fully featured programming language, allowing small programs to be written inside the mud, via OLC, without risking game crashes, or stalls. Each lensc program runs separately within the main mud engine. These programs are suspended after running a quota of instructions. They resume execution on the next mud pulse, after verifying that no data they were refering to has become invalid. This suspend-resume mechanism ensures that no builder can stall the mud with a lensc program.

Each lensc program can run multiple copies at a time. The programs compile into a bytecode stream that is interpreted as the programs need to be run. As the programs are never stored in bytecode format, changes to the bytecode process don't risk damaging the game when running older programs, the worst case is that a program that used to run may require a few changes. Lensc has a runtime debugger in the mud for analyzing a program line by line. The compiler produces error messages when a program does not compile. Attempting to run a program that is not compiling simply fails. Furthermore, whenever the lensc interpreter detects an error, it will terminate execution. Sometimes, this is desirable. If the mob that called the program is slain, the program must stop. Other times, programmer error is responsible. More on this can be found in the debugging section of this document.

Lensc is designed to be syntactically similar to C. It is not C however. The need to keep programs small, and protect the game integrity, prevents several C operators and constructs.

Immortals will be required to get approval for their programs until they have shown competence at programming. Immortals who have shown competence will have to approve any new program before it is allowed to be run through an action.

Back to top

Language Specification

A program is a collection of individual statements, executed in order. A language is the definition of what those statements are. Just like with a spoken language, knowing individual words does not mean the words are put together in any sensible order. "A red emu runs John at midnight." is a valid English sentence, but doesn't mean anything. Putting the sentences in the wrong order invalidates a program, just as it invalidates a sentence, "Emu midnight runs at red John a."

A lensc program follows a specified format. The program definition, function and global variable declarations, and finally the main program body.

Program definition

A program is defined with one line. It states the program name, and the data type of the caller. This line must come first in any program. Only char and obj types can be callers to programs.

	program name (datatype)
  
or
	program name (datatype,datatype)
  
The second form of prototype is for custom spell functions. The second datatype must match the expected target for the spell.

Variable Declaration and Data Types

top

A variable is a way to store and refer to a value that your program needs access to. Variables take memory space in the program, so only use the ones that are needed. Each variable has a specified type. Trying to assign a value of the wrong type to a variable will cause a program error and termination. The types supported are:

  • bool A true or false value
  • int An integer (non-decimal number)
  • string A line of text
  • char A character in the mud
  • obj An object in the mud
  • room A room in the game
  • exit An exit from a room
  • vehicle A vehicle in the game
  • area An area in the game
A variable is declared by stating the type of the variable, followed by an identifier (name). An optional assignment can be added after the identifier. You can specify multiple variables of the same type by separating the identifiers (and assignments) with commas, and end a declaration with a semicolon.
	int i;
	char ch;
	int i = 5;
	int i,j;
	int i=5, j=6,k=7;
	string j = "J";
  
Some variable types (those that refer to game elements like characters or objects) can be invalid. The variable exists, and can be used, but the data it refers to is not valid. Trying to use invalid data will cause a program to exit. There are functions that allow you to test whether a given variable refers to valid data.

Singly-indexed arrays of variables can also be declared. An array is a set of variables that share a name, and have different access numbers. Arrays are declared (and accessed) with square brackets. The number of elements is specified inside the array. Note, extremely large arrays may have the side effect of abnormal program termination.

	int arrayone[10];
  
Arrays cannot currently be initialized, they're set to all 0's.

Variables can be declared in any block within a program. A variable declaration is a standard statement. A good programming practice is to only declare variables in the block where they need to be used. This saves space on your 'stack'. Variables declared within a block can only be accessed within that block. Variabled declared outside of any blocks are considered "global" variables. Global variables can be accessed from any function.

Two global variables exist as part of every program. They are

	program type self;
	string argument;
	
These variables are initialized when the program is run. Self is either an obj or a char depending on the definition of the program, and is initialized to the item that ran the program. Argument is the text passed to the program from the calling action.

Some programs have an additional three variables:

	Target type target;
	int sn;
	int level;
	
These variables are available to programs that expect to be called as part of a custom spell, and are defined as such in their inital header.

Functions

top

A function is a sub-program that performs some role within a program. A function is declared with the word function, followed by an optional data type that the function will return to its caller. Finally, a list of parameters, with identifiers. Finally, a block of code defining what the function does.

	function one()
	{
	...
	}

	function int two()
	{
	...
	}
	function int three(int parameter1)
	{
	...
	}
  
Parameters are what the function takes as input. Within the body of the function, a parameter is refered to by the name it is declared with in the function's parameter list. When a function is called, the values of the parameters are set to the expressions resolved in the function call. Parameters are passed by value, so any changes to the parameters within the body of the function will not be reflected on variables outside the function body.

Main

Finally, the main body of the program, and where it starts executing, is the main block of code.

 	main()
	{
	...
	}
  

Comments

Interspersed in other code, you may always add comments. Comments are blocks of text surrounded with /* */ marks. They are ignored by the compiler, and serve only to remind the programmer why they did things.

Blocks

A block is a number of statements surrounded by curly brackets {}. Blocks are used to group statements together. A variable declared within a block can only be used within that block, although, variables declared outside the block can still be used within the block.

  	{
	statement
	statement
	statement
	...
	}
  

Statements

top

A statement is like a sentence. It is a complete thought in terms of programming. There are many different types of statements that can be used. Each represents a different sort of control in the program.

A statement can be any of the following: Bold words are keywords, while italics are elements that get replaced. More detail on each statement type follows later in this document.

  • block
  • expression;
  • variable declaration
  • if (expression) statement
  • if (expression) statement else statement
  • while (expression) statement
  • do statement while (expression);
  • for (expression;expression;expression) statement
  • return;
  • return expression;
  • break;
  • continue;
  • end;
  • end expression;

Expressions

top

An expression is anything that resolves down to one value. Mathematical and logical operators fall into this category, as does calling functions (both user-defined, and system functions).

Constants

A constant is the simplest form of expression. It is a value that is unchanging during the execution of the program. Constants can be numbers (5), hexadecimal numbers (0x0a), strings ("hello"), or LensC constants (TRUE).

  • Number constants can be placed in a program as numbers.
  • Hexadecimal number constants are specified with the first two characters 0x followed by a valid hex number (digits 0-9, letters a-f) and can be placed in a program as numbers.
  • String constants must be surrounded by double quotes. String constants support a number of text replacements within the string. These replacements all start with a backslash character, and are listed below:
    • \\ - place a single \ in the text string
    • \r - place a carriage return in the string
    • \n - place a newline in the string
    • \" - place a double quote character in the string
  • Game functions can be of different data types. They are case sensitive.

Input

LensC supports one additional method of getting input from an external mob or object action. An action can "signal" (see mobact and objact documents for details) the program. Within the program, the keyword instring(NUMBER) is used to access this input. The instring keyword is considered a string for all intents and purposes. Once the program encounters this keyword, a timer starts, and execution stops until input is provided. The duration of the timer is measured in seconds, specified in parenthesis. If no input has been found, the program will continue, treating the input as an empty string.

Variables

A variable is a value that can change as the program runs. Variables can be used in expressions either to retreive their current value, or to have new values assigned to them. Variables must have been declared before they can be used, as stated above in the variable declaration section. Arrays are a subset of variables. An array can be indexed with any standard expression.

	arrayone[1] = 1;
        arrayone[i] = i;
        arraytwo[i*3-2] = "Hello " + i*3;
  

Operators

top

Operators work on one or more existing values. Operators have precedences set to them. Precedence says that in a given series of operators, which should be resolved first. This allows series of instructions to be mathematically correct. Parenthesis can always be used to override default precedences.

	4 + 5 * 2 --> 14
	(4 + 5) * 2 --> 18
	
The available operators are:
  • ( expression ) - resolve what is inside the parenthesis first
  • int1 ` int2 - raise int-1 to the int-2th power
  • int1 * int2 - multiply int-1 by int-2
  • int1 / int2 - divide int-1 by int-2. Divison by zero is not allowed and will cause program termination.
  • int1 % int2 - modulous int-1 by int-2. Modulous means take the remainder of dividing the two numbers. Division by zero is not allowed and will cause program termination.
  • int1 | int2 - Bitwise OR operation.
  • int1 & int2 - Bitwise AND operation.
  • int1 ^ int2 - Bitwise XOR operation.
  • int1 << int2 - Bitwise LEFT SHIFT operation.
  • int1 >> int2 - Bitwise RIGHT SHIFT operation.
  • int1 + int2 - Add int-2 to int-1.
  • string1 + int or string - Add the second string or int to the end of the first string.
  • int1 - int2 - Subtract int-2 from int-1.
  • int1 == int2 - If int1 is equal to int2, result is 1, otherwise result is 0.
  • string1 == string2 - If string1 has same text as string 2, result is 1, otherwise result is 0.
  • data1 == data2 - If data1 has same value as value2, result is 1, otherwise result is 0.
  • int1 >= int2 - If int1 is greater than or equal to int2, result is 1, otherwise result is 0.
  • int1 <= int2 - If int1 is less than or equal to int2, result is 1, otherwise result is 0.
  • string1 <= string2 - If string1 matches the beginning of string 2, result is 1, otherwise result is 0.
  • int1 < int2 - If int1 is less than int2, result is 1, otherwise result is 0.
  • int1 > int2 - If int1 is greater than int2, result is 1, otherwise result is 0.
  • int1 != int2 - If int1 is not equal to int2, result is 1, otherwise result is 0.
  • string1 != string2 - If the text in string1 is not equal to the text in string2, result is 1, otherwise result is 0.
  • data1 != data2 - If data1 is not equal to data2, result is 1, otherwise result is 0.
  • ! value - If value is not 0, result is 0, otherwise result is one.
  • ~ value - Perform bitwise COMPLEMENT operation.
  • bool1 && bool2 - If bool1 and bool2 are both non-zero, result is 1, otherwise result is 0.
  • bool1 || bool2 - If bool1 and bool2 are both zero, result is 0, otherwise result is 1.
  • bool1 ? expresion1 : expression2 - If bool1 is 0, result is the result of expresion2, otherwise it is the result of expresion 1.
  • variable = expression - Assign the result of the expression into the variable. The datatype of the variable must be compatible with the resulting type of the expression.
  • int variable += expression - Add the result of the expression into the variable. The datatype of the variable must be compatible with the resulting type of the expression.
  • string variable += expression - Add the result of the expression onto the end of the string variable's text.
  • int variable -= expression - Subtract the result of the expression from the variable.
  • int variable *= expression - Multiply the result of the expression with the variable and assign the result to the variable.
  • int variable /= expression - Divide the variable by the result of the expression and assign the result to the variable.
  • int variable %= expression - Modulus the variable by the result of the expression and assign the result into the variable.
  • int variable `= expression - Raise the variable to the result of the expression'th power.
  • int ~= expression - Bitwise COMPLEMENT-assign
  • int |= expression - Bitwise OR-assign
  • int &= int2 - Bitwise AND-assign
  • int ^= int2 - Bitwise XOR-assign
  • int <<= int2 - Bitwise LEFT SHIFT-assign
  • int >>= int2 - Bitwise RIGHT SHIFT-assign
  • int variable ++ - Return the value of the variable, and then increment the variable by one.
  • int variable -- - Return the value of the variable, and then decrement the variable by one.
  • ++ int variable - Increment the variable by one and then return the value of the variable.
  • -- int variable - Decrement the variable by one and then return the value of the variable.
  • functionname(parameter list) - Call the named function. A parameter list is a comma-delimited series of expressions, one for each parameter the called function requires.
The precedence order of the operators is as follows (Highest priority is at the top. Multiple entries on a line mean equal precedence.
  • () ++ -- function-call
  • `
  • * / %
  • + -
  • >> <<
  • == >= <= > < !=
  • ~
  • &
  • ^
  • |
  • !
  • &&
  • ||
  • ?:
  • = += -= *= ^= %= /= ~= |= &= `= >>= <<=

Function Calls

top

Functions, as described above, are blocks of code that can be called with different parameters, that serve as sub-programs. When you call a function (by using the function name and parameter list as an expression), the program's execution goes to the function's code, and runs until the function returns. The type of value that a function returns is defined in the function definition, and can be used in further expression resolution.

	a = 5 * generic_math_function(3,4);
	
The parameter list is also defined by the function declaration. When calling a function, one parameter must be supplied for each parameter that the function has declared, and the parameter must be of the appropriate datatype. Using too few, or invalid typed values will cause a compilation error. Each parameter is separated by a comma. Parameters do not need to be constants, they can be any expression, including other function calls.
	function_one(5*4+a,function_two(20));
	

Complex Statement Types

If statements

top

The if statement is used for making decisions while the program runs. An IF statement has two or three parts. The first part is the expression to evaluate. This expression is compared to zero. If the result of the expression is not zero, then the conditional statement is executed. If it is zero, then either nothing, or the else statement is executed.

	if (a == 5)
	    sendt(ch,"A is 5\r\n");
	else
	    sendt(ch,"A is not 5\r\n");
	
If statements can control blocks of text, as a block is a type of statement.
	if (a==5)
	{
	...
	}

	if (a == b)
	{
	...
	}
	else
	{
	...
	}
	
If statements can also be nested, as if statements are statements.
	if (a == 5)	
	    sendt(ch,"5");
	else if (a == 6)
	    sendt(ch,"6");
	else if (a == 7)
	    sendt(ch,"7.");
	
While not required, it is standard practice to indent statements controlled by an IF for readability.

Switch statements

top

A switch statement is another way to do selective logic. In this case, you have a value, and a number of different options to try based on the value. A switch statement first specifies what expression is to be compared, and then provides for specific values to look for:

	switch (i)
	{
	case 1:
	    ...
	case 2:
	    ...
	case 3:
	    ...
	}
	
The program starts executing code from the first case it matches, and keeps executing until the end of the switch block, or until it finds a break statement. In this way, multiple values can be set to execute the same code easily:
	switch (i)
	{
	case 1:
	    ...
	    break;
	case 2:
	    ...
	    break;
	case 3:
	case 4:
	    ...
	    break;
	}
	
One additional label is provided, the default label. If a default is specified, it is executed if no specific matching value is found. If no default is found, and no matches exist, program execution continues following the switch block.
	switch (i)
	{
	case 1:
	    ...
	    break;
	case 2:
	    ...
	    break;
	default:
	    ...
	    break;
	}
	
There is no rule about what order the case values need to appear in, nor where the default appears within the switch body. A switch is more efficient than a series of nested loops, but less versatile; if statements are able to test ranges of values far more easily than switches. Knowing which to use in which situation takes a bit of practice. Switches can also be performed on strings or other value types.
	switch (direction)
	{
	case "north":
	    ...
	    break;
	case "south":
	    ...
	    break;
	default:
	    ...
	    break;
	}
	

Loops - general comments

top

Loops are a way to repeat a task until some condition is met. Computers are very good at doing repetative tasks. All loops have some control expression, and a loop statement (which can be a block of statements). Care must be taken to ensure that the control expression does eventually terminate, otherwise you get an "infinite loop". While LensC is safeguarded against infinite loops stalling the mud, they can still waste an excessive amount of CPU time, and should be avoided.

Two additional statements are available within the body of a loop block. They are break and continue.

The break statement breaks out of a loop, with control flowing to the next statement after the loop body. In the case of nested loops, the innermost loop is the one broken out of.

	while (loopcondition)
	{
	...
	    if (breakcondition)
	        break;
	...
	}
	

The continue statement returns to the top of a loop, skipping past any instructions not yet executed, and checks the loop's control expression again. Like with break, if there are nested loops, only the innermost one is continued through.

	while (loopcondition)
	{
	...
	    if (continuecondition)
	        continue;
	...
	}
	

While loops

The first loop construct is the while loop. This loop continues to execute a statment (or block of statements) as long as the control expression evaluates to a non-zero value.

while (control expression) statement

While loops require that the programmer explicitly change some values inside the body of the loop, so that the control expression changes. If the control expression is zero when the loop is first encountered, the loop statement is never executed.

	while (a < 10)
	{
	...
	   a += 3;
	...
	}
	
	int done = 0;
	while (!done)
	{
	...
	    if (a >= 10)
		done = TRUE;
	}
	

Do - While loops

A Do - While loop is similar to a while loop in that the loop continues to execute as long as the control expression is non-zero. The difference is that the code inside a do-while loop is guarunteed to execute at least once before the control expression is evaluated for the first time.

do statement while ( control expression);

Like a while loop, the programmer must explictly modify some variable within the loop body to ensure that the loop will eventually end.

	do a++ while (a<10);

	do
	{
	    a += 3;
	}
	while (a < 20);
	

For loops

For loops provide the most control of any looping construct. They allow for the initialization and modification of loop control variables outside the body of the loop. This can make a program much more readable, as the body of the loop is dedicated to the tasks the loop must do.

for (initial expression;control expression;modification expression) statement

The initial expression is evaluated when the loop is first encountered. It can be used to initialize a counter to any wanted value. The control expression is evaluated before each pass through the loop. If the control expression evaluates to zero initially, the loop body won't be executed at all. The modification expresion executes after the loop body has executed. It is used to alter some variable that needs altering each pass through the loop, often a counter. continueing out of a for loop will still evaluate the modification expression before checking the control expression again.

	for (i=0;i < 10;i++)
	    sendt(ch,"I is "+i+"\r\n");

	for (i = 1; i <= 10; i++)
	{
	...
	}
	

One aspect of a for loop, which is not true for the other loop types, is that you can omit any of the expressions that you don't need to use. Make sure that if you do omit any of the expressions that the loop will still terminate.

	for ( ; i>5 ; i++)
	{
	...
	}

	for ( ; ; )
	{
	    word = first_word(argument,WORD_FIRST);
	    argument = first_word(argument,WORD_REST);
	    if (word == "hello" || string_is_empty(word))
		break;
	}
	

Return

The return keyword is used to tell a function to return to its caller immediately, without running the rest of the function's code.

return;

	function test(int i)
	{
	    if (i<=0)
		return;
	    ...
	}
	
A function that is defined to return a value must use the return keyword to indicate what value is to be returned to the caller. The datatype of the returned value must match the datatype the function is declared to return.

return expression;

A program that is defined to return a value, and does not use the return command will return an invalid value, and risk program termination.

	function int minimum(int v1, int v2)
	{
	    if (v1 < v2)
		return v1;
	    else
		return v2;
	}
	

End

The end command is used to tell the program to terminate immediately. This is often useful if some necessary condition is not met.

The End command can also store a value which can be returned to another calling program (see Call function). This value can be of any type, but must match what the calling program expects.

end;
end expression;

	...
	if (a != c)
	    end a;
	else
	    end b;
	

Invalidate

The invalidate keyword tells the program that a given variable has become invalid, and should not be regenerated when rebuilding the handle table.

invalidate self;

This is rarely necessary, however, if your program destroys an object or a character, it may no longer reference them. If it attempts to, the program may crash. To prevent the program from terminating, invalidation of the handle is necessary.

Back to top

The Preprocessor

Lensc also includes a basic preprocessor that can be used to handle some text manipulation jobs. Currently, it supports two operations, including another program, and defining text replacements. To specify a preprocessor directive, you must specify the directive at the beginning of a line.

The first preprocessor directive is the ability to include the body of another program within your program. This can be useful to write commonly used functions in separate vnums and include those functions within your actual programs.

To include the body of another program, you need to know the vnum of that program. You then specify #include <vnum> where you would like that code included.

The second preprocessor directive is the ability to make simple text replacements. These are case-sensitive and context-insensitive (meaning, you could potentially replace all occurances of the letter 'a' with 'b' and it would let you - although your program would not compile as a result.

Replacements are called defines. To define a replacements, use the syntax: #define <text to replace> <replacement text>.

Defines are applied to full identifiers only. So, defining 'foo' will not cause a replacement in the word foobar, only when foo is found alone.

Defines are applied throughout the text of the program, including any included text, regardless of where they are defined.

A subset of the generic define is the macro. A Macro allows for variables to be inserted within the text replacement. Remember, however, that this is not true variable passing, it's text replacement. Good macros make sure that the parameters are wrapped within parenthesis.

A macro is defined the same way as any other define, however, you include a variable list with the definition:
#define amacro(x) (x)
or
#define amacro(x,y,z) (dice((x),(y))+(z))
. The replacement string can include as many or few of the variables as you want.
#define square_x(x,y) (x)*(x)
Notice that the y variable is is unused, while the x is used twice. These are both legal.

Defines are very useful for keeping your programs readable. You can define names to be used instead of numbers, allowing people to read meaningful constant names within the programs.

Compiling

To compile a lensc program, use the olc compile vnum command. This will either inform you that the program compiled successfully, or produce a list of program errors. Always start fixing errors with the first error listed. Subsequent errors may be the result of the compiler being confused due to the first error.

You can use the olc preprocess command to see the output of the preprocessor for a given program as well. If you are experiencing errors that you're having a hard time tracking down, this can be a useful tool for eliminating errors caused by unexpected preprocessor directives.

Back to top

Debugging

Lensc has a primitive debugger installed. This debugger allows you to run a program one line at a time, and watch certain data values as they occur in the program.

To start the debugger, you must have the program compiling without errors. If the program does not compile, the debugger will simply generate the messages that the compiler would generate. Having a program compile does not mean that the program works. Debugging is often required to ensure that the program does what you want it to.

To debug a program, enter debug vnum Caller argument. The vnum is, of course, the vnum of the program to run. The caller is the object or character which would normally run this program. This item or character can be anywhere on the mud, so be careful with what you enter. The determination of whether the debugger looks for an object or a character is based on the data type of the program running. The program refers to the caller as the self variable. The argument is the remained of the text on the line, and is referenced in the program as the argument variable.

Debugger commands:

  • Ins - Run one instruction
  • Step - Run to the next line of code
  • Runto line - Run the program to the specified line number
  • Trace - Run the program straight through.
  • Current - Shows the current instruction to execute, and registers
  • Show/List - Shows the program text.
  • End/Quit - Exits the debugger.
  • Help - Shows the command menu.
  • Globals - Shows all initialized global variables
  • Variables - Shows all initialized variables in the current function
  • Stack - Shows the program's runtime stack.
  • Handles - Shows the program's handle table.

Learning to use the debugger is a skill of its own. At its simplest, you can see which lines of code are being executed. I suggest debugging all programs completely before allowing them to be run by actions automatically.

Back to top