Writing:The Scripting Language

If you know Python, you’ll have recognized the script code in these examples as looking a lot like Python. In fact it is a lot like Python, but it’s not exactly Python. The Seltani server is written in Python 3, and I could have run all scripts directly in that environment. I chose not to, for two reasons:


 * It is notoriously difficult to run untrusted Python code safely. (Yes, I’ve looked at PyPy and so on.)
 * I wanted to extend the semantics of Python in some places, and limit them in others.

Here’s the compromise I settled on. Script code is parsed directly by Python, using the  library module. (Therefore, the script syntax is exactly Python 3.) However, the parsed code is executed by custom code (itself written in Python) which implements a very limited subset of the language, with a lot of safety checks. And, as I said, a few extensions. I’ll call this subset “TworldPy”.

What’s like Python
As I said, the syntax is Python 3, because I parse it with the Python 3 language parser. This means lines of code, no semicolons or other separators:

x = 1 function

A  sign marks a comment.

# This is a comment. x = 1  # This part of the line is also a comment.

Strings can be single-quoted or double-quoted, but not curly-quoted.

statements in Python look like this:

if x == 1: event("A.") else: event("B.")

The indentation is mandatory! That’s how Python knows where the  or   block ends. If you have several statements in the block, be sure that the indentation is the same for each.

"Since you’re editing in a web browser, you probably can’t enter a tab character. So you have to indent with spaces. Pick a consistent indentation style – I like two spaces per level – and stick with it."

A code property behaves like a function, so you can  from it. This does the same thing as the previous example:

if x == 1: event("A.") return event("B.")

What’s not like Python
The most important differences between stock Python 3 and my TworldPy language:

First, the global namespace consists of properties. That is, when you write a symbol like, the interpreter looks it up as a property in the script’s location and instance.

If the property is not found, the interpreter then checks the built-in symbol table. (This is how functions such as  and   are found.)

Symbols beginning with an underscore, such as, are special; they are treated as temporary (local) variables. You can freely assign to and read these values, but they are never written to the database. Temporary variables are discarded at the end of the code property that used them.

(Symbols used in a code property’s argument list are also local variables, no matter how they’re spelled. This is normal for Python.)

The  symbol (a single underscore) is even more special. It always refers to the built-in namespace, ignoring properties. You can assign to, but the assignment is ignored.

When you assign to a non-underscore, non-local symbol (e.g. ), the interpreter considers it a property write. This means that you cannot assign a value which is not database-storable, such as a Python set or function. (A line of code like  will fail, because   is a function and cannot be stored in the   property. However,   will work, because   is a temporary variable, not a property.)

A value is “database-storable” if it can be represented in BSON. BSON is a variant of JSON which is used by MongoDB, the database back end for Seltani. This covers basic types plus lists, dictionaries (but not sets), , and. Tuples are accepted but stored as lists.

The second major difference: a bare symbol reads a property and executes it, according to its nature. In stock Python, a line like  would do nothing (if   is a valid symbol). But in Seltani, it will examine  (if a   property), or execute it (if a   property), or moves the player (if an  ), or so on.

This only applies to statements which consist of a single symbol. In the earlier example, we saw the line:

sched(5, pumptimer)

In executing this, the  symbol is looked up, but not executed. It is merely passed as an argument to. However, if we’d written

pumptimer

...by itself, that would have executed the code immediately. (This is equivalent to calling .)

The rest of the changes of TworldPy are mostly about what’s missing. It has  and   statements, but no. You can’t. There is no way to define new classes. Comprehensions, generators, and lambdas are missing. And only a tiny slice of the standard library is available –,  ,  ,  , a few functions in   and.

I intend to add these features, as needed and as they arise in real life. It will be a slow process. Obviously, the primary concern is safety. Script code must never be able to corrupt the server environment.

(Inevitably, somebody will find a loophole at some point. Then we get to play the game of “database rollback.”)

Resource limits
If a script tries to do too much work in a single action, the server will kill it and report a RunawayException. This is intended to block infinite loops, infinite recursions, and other such dangers.

The limits currently include:


 * Calling functions (or code properties) nested 8 deep;
 * Having more than 16 events outstanding at a time in an instance;
 * Code that executes more than 500 (arbitrary) steps in one action.

These limits are deliberately tight. It’s quite possible that reasonable code will exceed them. Belford is willing to loosen them if necessary; let him know.

Other limitations
(See also the issues list on Github.)

Strange things can happen if you try to edit a list or dictionary Value property. (Issue #97.) However, setting such a property should work, and in-place mutation of temporary variables works correctly. Thus, the following workaround: foo = ['a', 'c', 'd', 'e'] foo.insert(1, 'b')
 * In-place list mutation
 * 1) This doesn't work:

foo = ['a', 'c', 'd', 'e'] _tempfoo = foo _tempfoo.insert(1, 'b') foo = _tempfoo
 * 1) Do this instead: