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
ast 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()
# 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.
if 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
else 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
return 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
count, 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
text() are found.)
Symbols beginning with an underscore, such as
_foo, 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.)
_ 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.
foo = 3), 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
foo = text will fail, because
text is a function and cannot be stored in the
foo property. However,
_foo = text will work, because
_foo 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 (
str) plus lists, dictionaries (but not sets),
ObjectId. 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
foo would do nothing (if
foo is a valid symbol). But in Seltani, it will examine
foo (if a
Text property), or execute it (if a
Code property), or moves the player (if an
Event), or so on.
This only applies to statements which consist of a single symbol. In the earlier example, we saw the line:
In executing this, the
pumptimer symbol is looked up, but not executed. It is merely passed as an argument to
sched(). However, if we’d written
...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
return statements, but no
try/except. You can’t
import. There is no way to define new classes. Generators and lambdas are missing. And only a tiny slice of the standard library is available –
len, a few functions in
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.”)
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 10 deep;
- Having more than 16 events outstanding at a time in an instance;
- Code that executes more than 500 (arbitrary) steps in one action.
- Data structures (dicts and lists) nested more than 8 levels deep cannot be stored in the database. (They can be manipulated in local variables.)
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.
(See the issues list on Github.)