Serpent is a programming language inspired by Python. Like Python, it has a simple, minimal syntax, dynamic typing, and support for object-oriented programming. Serpent also draws inspiration from XLisp, Squeak, SmallTalk, Ruby, and Basic. Why another language? Serpent is designed for use in real-time systems, especially interactive multimedia systems. Serpent is unique in providing the following combination of features:
I recommend Python to anyone who does not need these features. Serpent has some other differences from Python that reflect both the designer's personal tastes and limited resources:
Serpent has the following primitive types:
and the following structured types:
and the following types that users ordinarily don't think of as types:
There are several other types used internally that are not visible to programmers. There is no Boolean type. Instead, the special value nil represents false, and the symbol t represents true. The global variables true and false are bound to t and nil and should be used for readability.
Serpent syntax, like that of Python, uses indentation to indicate structure. Statements are grouped, not by brackets, but by indenting them to the same level, each statement on a separate line. If a statement "group" has just one statement, it does not need to go on a new line, but this is not yet implemented.
Serpent expressions use the following operators, listed in order of precedence:
., [], **
(unary) +, (unary) -, ~, not
*, /, %, &
+, -, |, ^, <<, >>
<, <=, =, !=, >, >=, is, is not, in, not in
and
or
Other expressions are formed by calling methods and functions. Parameters are passed by value, and functions may have optional, keyword, and arbitrary numbers of parameters. Keyword parameters are passed by writing the keyword followed by "=", as in:
foo(size = 23)
Only parameters declared as keyword or dictionary parameters can be passed using the keyword notation. All other parameters are positional.
Any expression can be used as a statement.
Statements should be familiar to programmers, so they will be described here by example and at most a few comments. Note that because of the indentation-based grouping, semicolons are not used (except in the print statement for formatting).
The "load" statement loads a file, executing each statement and compiling each function and class sequentially in the order read from the file. The file may contain its own "load" statements, which may be immediate commands at the top level or run-time load commands excuted within a function or method. The standard file extension is appended to the filename if no file is found without the extension.
load "myfile"
identifier = expression
an_array[index] = expression
an_object.field_name = expression
if condition1:
stmt1
stmt2
elif condition2:
stmt3
stmt4
else:
stmt5
stmt6
There are three forms of iteration constructs, starting with the familiar "while":
while condition:
stmt1
stmt2
In the first "for loop" form, the "variable" must be declared as a local variable, and the "by" part is optional:
for variable = expression1 to expression2 by expression3:
stmt1
stmt2
In the second "for loop" form, "expression1" must evaluate to an array, and "variable" is bound to each value in the array:
for variable in expression1 :
stmt1
stmt2
At present, there are no equivalents to C's "break" and "continue" statements, but these will be added.
print expr1, expr2 , expr3; expr4
The print statement writes characters to the standard output, which is the file whose handle is the value of global variable stdout. Each expression is printed. A comma outputs one space while a semicolon outputs no space. If there is no trailing comma or semicolon, a newline is output after the last expression in the print statement. A print statement has the value nil.
return expression
The expression is optional; nil is returned if no expression is provided.
There is no exception mechanism, but one will be added.
Serpent functions and classes are created by declaration. Within classes, member variables and methods are declared. Within functions, local variables are declared. Global variables do not need to be declared. Symbols and global variables are equivalent: every symbol has a slot to hold the value of a global, and every global is implemented by creating a symbol.
Simple, positional parameters are declared in the parameter list by simply naming them with comma separators:
def foo(p1, p2, p3):
Parameters can also be specified as optional (the parameter can be omitted, a default value can be provided), keyword (the formal parameter is named by the caller, the parameter may have a default value), rest (there can only be one "rest" parameter; it is initialized to an array containing the value of all left-over actual positional parameters), dictionary (there can only be one "dictionary" parameter; it is initialized to a dictionary containing the values of all left-over keyword parameters), and required (standard positional parameters).
def bar(p1, optional p2 = 5, keyword p3 = 6, rest p4, dictionary p5):
This function could be called by, for example:
bar(1, p3 = 3, d1 = 4), or
bar(1, 2, 3, 4, 5)
def foo(p1, p2, p3):
var x
stmt1
stmt2
return expression
Functions return the value of the last statement if there is no return statement. Remember that statements may be expressions, allowing a functional style:
def add1(x):
x + 1
As shown above, local variables are declared using "var". Locals may be initialized, and multiple locals can be declared with a single "var" declaration. The declaration may occur anywhere in the function, but it must occur before the first use of the variable.
var x = 1, y = 2
class myclass(superclass):
var instance_var
def method1(p1):
instance_var = p1
Classes specify instance variables (without initialization) and methods, which look just like function declarations except they are nested within the "class" construct. A class may inherit from one superclass. Within a method, the keyword "this" refers to the object. To create an object of the class, use the class name as a constructor:
x = myclass(5)
When an object is created, the init method, if any, is called, and parameters provided at object creation are passed to init. The init method return value is ignored, so it is not necessary to explicitly return this.
Member variables may be accessed directly using "." as in x.instance_var.
When a built-in operation encounters the wrong types and the first argument is an object, then the call is converted into a method lookup. (This is not implemented for all primitives yet.)
It is illegal to define a global function with the name of a built-in function.
Serpent does not have first-class functions. Instead, functions are represented by atoms (call the corresponding global function) or by objects (call a method of the object).
Serpent has additional statements to support Aura message passing:
connect object-id from object-id
connect object-id to object-id
set attribute to value after delay
object-id <- set attribute = value at timestamp
set attribute to value
As illustrated by these examples, the "<-", "at", and "after" parts are optional. Attributes are named by symbols. A Serpent dictionary maps a symbol to the corresponding Aura object, and values are converted to match the type of the attribute.
The classes "Aura_object" and "Aura_active" are built-in and may be subclassed. When a subclass is defined, a new Aura prototype is created, the class name (as a symbol) is added to the Serpent/Aura dictionary with a reference to the prototype, and the Aura prototype is initialized to forward messages to the new class.
Subclasses of "Aura_object" and "Aura_active" may have methods defined by the following form:
on attribute:
statements
This declares a method to be called when the attribute is set (the method is named "_on_" appended to the attribute name. An instance variable is also declared if it does not already exist.
A dictionary is used to specify the interface between Aura messages and the Serpent object. The dictionary is stored as the instance variable aura_interface. The dictionary is a mapping from attributes to arrays with the following elements:
To build the aura_interface dictionary, every class should provide a method named build_aura_interface that constructs the interface dictionary. This method is called when the class is defined in order to create the prototype object.