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.
Generally, statements may be combined on one line by separating them with a semicolon. If two or more statements are separated by a semicolon, all statements are considered to have the same indentation level as the first one.
Certain control constructs may include statements on the same
line after the colon rather than indented on the next line. These
are def, if, elif, else, for, and while.
Real constants are decimal numbers with a decimal point, e.g. "56" is an Integer while "56." is a Real. Reals can also have an exponent suffix denoted by "e" or "E" followed by an integer exponent. "4.5e2" means 4.2 times 10 to the power of 2. If an exponent suffix is present, the decimal point is optional.
String constants are any character string enclosed in double
quote (") characters.
Within the string, two double quote characters ("") represent a single
double quote (")
character (a single double quote character would terminate the
string.) Alternatively, a double quote can be inserted into a
string by escaping it with a backslash character (\). The backslash
character is used with other characters to denote special
characters in strings:
There is no character type in Serpent. Instead, characters are represented by strings of length 1.
Symbols are unique strings. Each symbol is stored in a system
dictionary, and symbols are associated with a global value and a
global function. Thus, every symbol is automatically a global
variable that can be assigned to, and global variables are
declared merely by being mentioned in the program. Symbol
constants are like strings, but single quotes are used as
delimiters. The same escape codes are valid. Thus, although not
recommended, symbols can contain double quotes (no backslash is
necessary for escaping this) and even single quotes can be
inserted using the backslash, e.g. 'symbol\'with\'single\'quotes'
.
[1, 'a', "hi"]
evaluates to an array with an Integer, Symbol, and String element
(in that order). Array expressions are constructed at run time
(unlike in Python) because the result is mutable. Therefore, if your
program needs a large constant array, e.g. a look-up table, you
should construct it once and assign it to a global variable. Then,
at the point where the array is used, use the global variable to
avoid recreating the table each time you need to use it.
A dictionary is denoted by a list of key/value pairs within curly
braces, e.g. {'top': 50, 'bottom': 100, 'left': 10,
'right': 90}
. Note that the key/value pairs are separated
by commas, and a colon separates each key from its corresponding
value. Also note that keys are expressions that are evaluated;
without the quotes, a symbol such as 'top' would be evaluated as a
global variable named 'top' and raise an error if not initialized.
for
clause and an optional if
clause. In that case, the for
clause (defined below) is executed, and for each iteration, the
expression is evaluated to become the next element of the array. (So
this is more of an “array comprehension,” than a “list
comprehension” unless you call dynamic arrays “lists” as does
Python.) In addition, the for
clause can be followed
by an if
clause, in which case the if
condition is evaluated on each iteration, and only when true is the
expression evaluated and appended to the array. A for
clause takes any of the forms of for
statements (see
Section “Iteration” below), only the clause is in-line (not on a
separate line) and is never terminated with a colon. An if
clause is simply the keyword if
followed by an
expression (the condition). For example, the following constructs a
list of integers from 0 through 9:
[i for i = 0 to 10]
The following constructs a list of factors of 144:
[i for i = 1 to 145 if 144 % i == 0]
The following constructs a list of symbols from a list of strings:
[intern(s) for s in ["string1", "string2"]]
The following illustrates the feature that multiple elements can
have for
clauses. This expression evaluates to an
array with integers from 0 to 9 followed by even integers from 10
through 18:
[i for i = 0 to 10, i for i = 10 to 20 by 2]
Finally, the comprehension expressions are just expressions, so they
may be nested. The following constructs an array of arrays of
increasing length:
[[i for i = 0 to n] for n = 5 to 10]
This last "., [], **
(unary) +, (unary) -, ~, not
*, /, %, &
+, -, |, ^, <<, >>
<, <=, ==, !=, >, >=, is, is not, in, not in
and
or
if else
if else
" "operator" is used in expressions
of the form expr1 if
condition else
expr2. The condition is evaluated first. If it is true, the
first expression is evaluated to form the value of the overall if
else
expression; otherwise, the second expression is
evaluated to form the value.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.
load "myfile"
require "myfile"
The difference between load
and require
is that load
always compiles and executes the contents
of the file, whereas require
first checks to see if
the file has been loaded (or "required") previously. If so, the file
is not loaded again. Use load
when the file contains a
script of actions that may be performed multiple times. Use require
for files that contain class and function definitions that should be
loaded at most one time.
A search path is used to locate source files. Under Windows, the search path is stored in the registry as the SERPENTPATH value in HKEY_LOCAL_MACHINE/SOFTWARE/CMU/Serpent. Under Linux and Mac OS X, the environment variable SERPENTPATH holds the search path.
The search path is a list of paths separated by colons (:) in
Linux or OS X or by semicolons (;) in Windows.
Please read these installation notes
for more details on setting up a search path.
The search for a file begins by testing the supplied filename for a ".srp" suffix. If none is found, ".srp" is appended to the filename. Then the compiler tries to open the file in the load directory. The load directory is current directory at the time Serpent was started, or, if an initial file was specified on the command line, the directory of that file. If the file is not found in the load directory, the file is searched for sequentially in each directory in the search path until the file is found.
A list of loaded files is kept in an array bound to the global
variable files_loaded
. The only the filenames with
extensions are stored here. Therefore, when a require
command is executed, Serpent can check to see if the file has been
loaded without appending each path from the search path.
A list (array) of path names of the currently loading files is
kept on the global variable paths_currently_loading
.
When a nested load begins, the new path is appended to the array,
and when the load completes, the path is popped from the end of
the array. An empty array indicates that no load is in progress.
identifier=
expression
an_array[
index] =
expression
a_dictionary[
key] =
expression
an_object.
field_name=
expression
Assignments may be followed by semicolon (;), and if there is a trailing semicolon, another statement may follow on the same line. Putting a statement after a semicolon is equivalent to putting the statement on the next line at the same indentation level as the current line.
if
condition1:
stmt1
stmt2
elif
condition2:
stmt3
stmt4
else:
stmt5
stmt6
Colons are optional. If colons are present, one or more semicolon-separated statements may follow a colon on the same line. If there is at least one statement after a colon, there must not be an indented statement on the next line.
In the first "for loop" form, the "variable" may already be declared as a local variable. If not, a local variable is declared (so a subsequent local declaration is not allowed). The "by" part is optional and defaults to 1. The variable is initialized to the value of expression1. The direction (up or down) depends on the value of expression3 (zero or positive is "up", negative is "down"). For the "up" case, the loop exits if expression1 is greater than or equal to expression2. For the "down" case, the loop exits if expression1 is less than or equal to expression2. (Expression2 is evaluated only one time.) If the loop does not exit, the statements are evaluated in sequence. Then expression3 (which is only evaluated once) is added to variable. This cycle of test, execute statements, and add is repeated until the loop exits. It is not an error if expression3 is zero.while
condition:
stmt1
stmt2
The rules for colon and statements on the same line as the colon introduced above forif
-elif-else
also apply towhile.
In the second "for loop" form, "expression1" must evaluate to an array, and "variable" is bound to each value in the array, and "index" is bound to the index of "variable" in "expression1". The "at" part is optional, in which case the index or loop interation count is not available unless you add extra code to track it.for
variable=
expression1to
expression2by
expression3:
stmt1
stmt2
At present, there are no equivalents to C's "break" and "continue" statements, but these may be added in the future. An alternative to "break" is to put the loop in a function and return rather than break. An alternative to "continue" is to put the rest of the loop body (that would be skipped by "continue") in a conditional ("if") construct.for
variableat
indexin
expression1:
stmt1
stmt2
if
-elif-else
and while
also apply to for
.
The display statement is very handy for debugging and similar to thedisplay
label,
expr1,
expr2,
expr3
print
statement. The “label,” which should be a quoted
string, is printed followed by a colon. Each expression is printed twice,
first as if it were quoted, folllowed by an equal sign, and
second as if it were an ordinary expression that is evaluated to
yield a value. Expressions are separated by commas. For
example,
display "in my_sort", x, y
might print:
in my-sort: x = 1, y = foo
If the statement is followed by a comma (,) a comma is printed
rather than a newline. A display
statement, with or
without a trailing comma, may be followed by a semicolon and another
statement on the same line.The print statement writes characters to the standard output, which is the file whose handle is the value of global variable,
expr2,
expr3;
expr4
stdout
.
Each expression is printed, using the equivalent of
str(expr)
to convert an expression to a string if it
is not a string already. 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
.Because a trailing semicolon is meaningful for formatting, a
semicolon (;) after print
is used as a format
specification. If there are a comma followed by semicolon
or two consecutive semicolons (with or without spaces),
the first character (comma or semicolon) is a formatting character
(so no newline is printed), and the final semicolon terminates the
statement, allowing (optionally) another statement to follow on
the same line.
return
expression
The expression is optional; nil
is returned if no
expression is provided.
def foo(p1, p2, p3):
Parameters can also be specified as required (standard positional
parameters), 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),
and dictionary (there can only be one "dictionary" parameter; it is
initialized to a dictionary containing the values of all left-over
keyword parameters).
def bar(p0, required p1, optional p2 = 5, keyword
p3 = 6, rest p4, dictionary p5):
This function could be called by, for example:
For optional and keyword parameters, a default value may be provided. The syntax is "bar(1, p3 = 3, d1 = 4)
, or
bar(1, 2, 3, 4, 5)
=
expr" where expr
is either a constant (a number, a string, or a symbol) or a global
variable identifier. If the value is a global variable identifier,
the value of that variable at compile time is used. If the
value changes at run-time, this will have no effect on the default
parameter value. The expr may not be an expression involving
functions or operators.Formal parameters must be declared in the order:
required, optional, keyword, rest, dictionary.
The body of the function can be a statement list after the colon
(:), statements being separated by semicolons. If there are no
statements on the same line, the colon is optional.
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 foo(p1, p2, p3):
var x
stmt1
stmt2
return expression
def add1(x):
x + 1
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. All instance variables and methods are inherited and fully accessible from within or from outside the class.
Within a method, the keyword this
refers to the
object. You can call methods in the class by writing this.some_method(some_parameter)
,
but you can also simply write some_method(some_parameter)
,
and if some_method
is defined in the class or
inherited from a superclass, it will override any global function
by the same name and will be invoked as a method on this
.
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
.
(init
may be inherited from a superclass). The init
method return value is ignored, so it is not necessary to explicitly
return this
. Within the init
method of a
subclass, there should ordinarily be a call to initialize the
superclass. The special variable super
refers to the
new object being instantiated as if it were an instance of the
superclass. (In the same way that this
refers to the
current object in the current class, super
refers to
the current object in the superclass). To call the superclass's
initialization method use ordinary method invocation syntax and with
parameters appropriate to the superclass's init method. For
example, if the superclass is
myclass, and the init
method of myclass
takes one argument, then there
should be a call that looks like super.init(5)
. The
return value of this call should be ignored.
Member variables may be accessed directly using "." as in
x.instance_var
.
Methods are invoked in a similar fashion: x.some_method(
parameters)
.
Methods defined for a class can have the same name as methods in
a superclass. These methods will override the superclass methods.
You can access inherited methods (even ones that are overridden by
methods defined in the current class) by refering to the current
object as super
. Thus super.meth(
parameters)
will search for meth
starting in the superclass
method dictionary, ignoring any definition of meth
in the current class. (This is just a more general view of the
"trick" used to call a superclass's init
method
explained above.)
Debugging in Serpent uses compiler messages, run-time error messages, a built-in debugger, and most of all, print statements. Serpent has a very fast compiler, so when an error is encountered, the compiler simply stops compiling and prints an error message. The error message tells you a file name and line number and possibly more. The line number reflects where the error was detected as the file was sequentially processed. The location of the actual error may be before the location where an error is detected.
Run-time error messages occur for many reasons: attempting to access an uninitialized global variable, dividing by zero, an out-of-bounds array index, passing too many or too few parameters to a function, type errors such as using a string where a number is required, etc. When a run-time error occurs, an error message is printed. A line number is printed, but it corresponds to the location of the next instruction. The location of the error may be an earlier line. For example, if the error message reports an array access problem at line 30, but there is no indexing, e.g. "[expr]" on line 30, you should look backwards for an expression with indexing.
The debugger is very simpler, but very useful. Every program
should say:
require "debug"
The debugger is just Serpent code. The main limitations are
that the debugger cannot single-step or set breakpoints. Instead
the debugger is invoked when a run-time error occurs. The debugger
can then print out a stack trace (most useful), print the values
of local variables, move up and down the stack to examine
variables in different stack frames, and resume execution. Type
RETURN for a brief summary of commands.
Under wxSerpent, if the debugger is started, it will prompt for input in a pop-up dialog box, which may be confusing. See “Error Handling and Debugging” in the wxSerpent page for special considerations when running wxSerpent.
The most useful debugging tools are print
and display
.
Do not be afraid to put display
statements in
Serpent library programs to help understand how they work.
The display
command was especially created for quick
debugging output.
If you required "debug"
in you program, you can
call breakpoint(1)
anywhere in your program. When
this call is executed, the debugger will run as if an error occurred,
but you can resume by typing ">"
. The parameter
can be any integer to break on the nth call, or if not an integer,
then break occurs on non-nil values so you can use a conditional
expression to decide when to break.
Each place where breakpoint
is called in the source
code has a separate counter.
You can tell the debugger to run some code when you exit if you need
to clean up something, and you can make stack trace printing
optional. See comments in serpent/lib/debug.srp
.
elif
and else
keywords, except grouping is not based on
indentation but rather on a closing #endif
directive, and
since this operates at compile time, there is no requirement for
branches not taken to even compile; thus, you can direct the compiler
to skip blocks of documentation or other text. The format is:
#ifdef symbol ...any text... #elifdef symbol ...any text... #else ...any text... #endifwhere the
#
directives must appear in the first column
and symbol
is any Serpent variable name. Unlike a Serpent
conditional that tests an expression for true or false, this tests
if symbols are bound to any value. Being unbound (unassigned) is
treated as false, and is not an error.
There are a number of other compiler directives.
#if
expression
and
#elif expression
evaluate expression at the point in compilation where the directive
is reached and either processes or skips the immediately following
text.
#ifndef symbol
and
#elifndef symbol
are similar to #ifdef
and #elifdef
except
they test if symbol is not bound.
Sometimes, it is useful to disable debugging, testing, or logging functions in production code while leaving the function calls in place for possible use in the future. Of course you can do this with tests at runtime, but Serpent has a way to eliminate even this overhead by declaring functions to be "no ops":
#noop
symbol
declares symbol to be a function that does nothing.
Subsequent calls to symbol encountered by the compiler
will not result in any parameter evaluation or function call, thus
no-op function calls can only be compiled as statements. A no-op
function call within an expression has no return value and thus will
not compile.
A function definition (def
) is ignored if the function
name has been declared with #noop
.
In Serpent, if there is no return statement, the function return value is the value of the last expression evaluated. This still applies with no-op functions. Consider the following:
// #noop bar def bar(): 1 def foo(): 2 bar()Note that
foo()
normally returns 1, but if we uncomment the
#noop
directive, then foo()
will return 2
because bar()
is not evaluated. (It is not even compiled.)
You can reinstate a no-op function name with the directive:
#yesop
symbol
although this could make code very difficult to read and should be
used with caution. Even after #yesop
, the reinstated
function may need to be compiled, and all functions calling it need to
be recompiled as well.
serpent/extras
for Sublime and Emacs support files.
Use spaces around all operators: x = a + b / c
, not
x=a+b/c
or even x = a + b/c
.
Use space after commas to separate parameters:
f(x, y, z + 3)
, not f(x,y,z + 3)
.
Use blank lines carefully. With longer functions, blank lines can be used to separate logical blocks of code. Often you will want to put a one-line comment at the beginning of such blocks to describe what the block does:
// create a new named widget var w = Widget(name) widget_table[name] = w // add the widget to the display display_list.add(w) display_list.update_layout()Since functions and methods have embedded blank lines, it is best to use at least two blank lines between functions and methods. If methods are separated by double-spaces, perhaps classes and function definitions at the top level should be separated by three blank lines. (This can look too spread out; if it does not help readability, less space often looks better.)
And speaking of comments, use a space after comment tokens:
// good comment with space after "//" //do not run comments into "//" # another good comment #this comment is missing a leading spaceBoth "//" and "#" are acceptable comment tokens. Since "#" is darker, I prefer to use it sparingly and sometimes to call attention to important points or key 1-line summaries of functions.
+
to join them:
"Pretend this is a very long string of text that requires two lines."
could be written as:
"Pretend this is a very long string " +
"of text that requires two lines."
Long lines also occur in print
statements. You can use
a trailing comma or semicolon to avoid printing a newline, then
continue printing with another print statement:
print "Imagine that this statement is too long for one line", var1, var2
This can be rewritten as:
print "Imagine that this statement is too long for one line",
print var1, var2
Note that the 2nd print, which is logically part of the same
output, had 5 spaces after print
so that the data to
be printed is indented relative to that of the first print
statement. (You cannot indent print
itself, so this
is a way to show subordinate information or a continuation of the
first print statement.)
Similarly, display
statements have a trick for long
lines:
display "Imagine this is too long for one line", var1, var2
If a display
statement ends in a comma, no newline is
printed. If a display
statement does not begin with a
literal string, it will not generate an initial label from the
string, so the following can be used to break the statement into two
lines:
display "Imagine this is too long for one line",
display var1, var2
and the output will be something like:
Imagine this is too long for one line: var1 = 1, var2 = 2
Again, extra spaces help the reader understand the continuation.
Of course, it can be inconvenient to produce very long output lines
as well. If you have one message to output across multiple lines,
you can indent lines after the first, e.g.
print "Imagine that this statement is too long for one line"
print " "; var1, var2
which will print something like:
Imagine that this statement is too long for one line
1 2
With display
you more-or-less have to use the initial
label, but it can be blank, as in:
display "Imagine this is too long for one line", var1
display " ", var2
and the output will be something like:
Imagine this is too long for one line: var1 = 1
: var2 = 2
//
or #
.
Always follow //
or #
with a single
space before the comment (or multiple spaces if you want to form an
indented block of text.) In the author's opinion, //
is prettier than and preferable to #
. You can use #
,
which is darker, for emphasis, e.g. to mark code that needs
attention or to provide a one-line summary of each function.
Full-line comments should generally precede the thing commented on,
and end-of-line comments should follow the thing commented on, e.g.
// pr_range - return integer from low through high - 1
def pr_range(low, optional high)
int(pr_unif(low, high)) // truncate real value
Variable, class, and function names should be descriptive. It is worth a minute to globally rename variables and naming conventions if it makes your code more readable and consistent!
If you want to be consistent with Serpent
libraries, use underscores for multi-word variable names, e.g.
file_length
,
and use lower-case everywhere except to capitalize classes, e.g.
Labeled_slider
. Use all-caps for global
“constants,” e.g. NOTE_ON = 0x90
.
If you
must use “CamelCase,” at least start with lower
case for variables and functions, and upper case for classes,
e.g. var fileLength
and class LabeledSlider
.
Therefore, programs should have a very careful explanation of the implementation and an argument that the implementation will work correctly. I often write this first before any code, and often spend as much time maintaining the comments as the code. Put the high-level explanation at the top of the program. A similar treatment, on a smaller scale, can be given to each function by putting a block of comments just before the function.
abs(x)
x & y
s.append(x)
apply(function, argarray)
array(n, fill)
array(10,
array(10, 0))
to construct a (defective) 10x10 array, use
[array(10, 0) for i = 0 to 10]
. This executes the
expression array(10, 0)
10 times to make 10 separate rows.atan2(y, x)
chr(i)
a.clear()
f.close()
f.closed()
codepoints(s)
a.copy()
cos(x)
s.count(x)
getcwd()
dbg_gc_watch(x)
x
. Only one address may be watched. This
only has an effect when Serpent is compiled with the flag
_GCDEBUG
. Note that x
is an
integer. See id
.error()
, except that the debugger (if any)
is not invoked. This should only be executed by a debugger to
escape back to the top level command prompt.dict(n)
error(s)
x ^ y
exit([i])
exp(x)
f.fileno()
find(string, pattern [, start [,
end])
fromcodepoints(array)
iscodepoints
.)flatten(array)
f.flush()
fork()
fork
to the original caller, and nil
is returned as if fork
was also called by the new
thread. The new thread will have a shallow copy of the current
stack frame; thus, it inherits all local variables by value (and
if a local variable has a dictionary or array, these values are
shared with the original thread.) When the new thread encounters
a return statement or the end of the method or routine that
called fork
, the new thread is deleted.frame_class(frame)
frame_object(frame)
this
) of the method of
the frame. The result is nil
if the method is a
global function.frame_get()
frame_method(frame)
frame_pc(frame)
frame_file(frame)
frame_char(frame)
frame_file()
).frame_previous(frame)
frame_variables(frame)
funcall(function, arg1, arg2,
...)
gc_cycles()
a.get(k [, f])
getenv(key)
getfeatures()
get_os()
get_slot(object, symbol)
set_slot(object, symbol, value)
a.has_key(k)
hash(object)
hex(i)
id(object)
idiv(i, j)
s.index(x)
inrange(x, low, high)
(low ≤ x and x ≤ high)
s.insert(i, x)
int(x)
intern(string)
isalnum(s)
isalpha(s)
isarray(a)
isatom(x)
iscodepoints(array)
isdict(d)
isdigit(s)
isdir(path)
isinstance(object, class)
isinteger(n)
islower(s)
isnull(x)
x == nil
isnumber(n)
isobject(obj)
isreal(n)
isspace(s)
issubclass(class1, class2)
isupper(s)
issymbol(s)
isstring(s)
join()
fork
). Children threads are
threads created by this thread.jointo(thread)
fork
).
a.keys()
s.last()
len(s)
len(d) == len(d.keys())
). listdir(path)
path
(a string).
The result is an array of strings, or nil if the path does not name a directory.iseven(x)
isnegative(x)
isodd(x)
ispositive(x)
log(x)
max(x, y)
min(x, y)
mkdir(path)
f.mode()
f.name()
~x
object_class(obj)
isinstance(obj, Bar)
and you assign Foo =
Bar
, then 'Foo'
and 'Bar'
denote the same class, so isinstance(obj, Foo)
becomes
true. On the other hand, if you assign Bar = nil; Foo = nil
,
then isinstance(obj, Bar)
becomes false, and
object_class(obj)
will return nil
.object_variables(obj)
obj
and their values, including all instance variables
inherited from superclasses.oct(i)
open(filename, mode)
x | y
nfkc(s)
ord(c)
pow(x, y)
x ** y
random()
random_seed()
.random_seed(i)
random()
using i
,
a positive integer. To avoid getting the same sequence of random
numbers normally generated by random()
,
call random_seed(time_date()[0])
or random_seed(int(time_get()
* 1000))
or use some other non-deterministic value for
the seed.f.read([size])
read
returns
empty string if end-of-file is reached.
If an invalid UTF-8 sequence is found, behavior is not specified.
Ideally, valid UTF-8 should be returned up to the point where the
next character cannot be decoded. Then the next call should
return
'NOTUTF8'
(named for the ISO C error code). Subsequent
reads will resume at the next convertable character.
The read may return more than size
bytes,
and the read may return less than size
UTF-8
characters because they require up to 4 bytes each.
In binary mode, read
returns an array of zero
(on EOF)
or more, but no more than size
, bytes (see Raw or Binary File I/O).f.readline([size])
nil
if end
of file is reached. Raises an error if file was opened in binary mode.
If an invalid UTF-8 character is
encountered, behavior is undefined, but should
return characters up to that point as a string. The
next call to readline
will return the symbol
'NOTUTF8'
and the next call will resume reading
at the next byte where a UTF-8 character can be decoded.f.readlines([sizehint])
readlines
should return 'NOTUTF8'
, and on the
next one, resume reading at the next UTF-8 character.
Note: To determine that the
whole file is successfully read, even if there is no size
parameter, make another call to read to readlines
and check for an empty list.f.readvalue()
nil
if
no value is found. If invalid UTF-8 is encountered, results
are not defined, but readvalue
should return nil
.
Raises an error if
f
was opened in binary mode. real(x)
rem(n, m)
a.remove(x)
rename(oldpath, newpath)
repr(object)
s.resort([f])
f
compares two elements of s
(see sort). Elements of
s may be arrays, in which case the first element of each array
is the sort key.s.reverse()
rmdir(path)
round(x[, n])
resume(thread)
yieldto(thread)
.)runtime_exception(msg, frame)
lib/debug.srp
If this
function is defined, it is called when a runtime exception
occurs. The msg is a text description of the error, and
frame is the stack frame for the function or method where
the error occurred (see frame_...
functions to
retrieve the instruction, stack, local variables, etc.)runtime_exception_nesting()
f.seek(offset, whence)
send(object, method, arg1, arg2,
...)
sendapply(object, method,
argarray)
s.set_len(n)
x << n
x >> n
set_real_format_precision(precision)
set_real_format_type(letter)
set_symbol_value(s, v)
sin(x)
sizeof(object)
s.sort([f])
sqrt(x)
str(value)
to_str()
method, the method is invoked. Otherwise, if the object has a
to_string()
method, it is invoked. If neither
method exists, or if to_str()
returns
nil
, a string is generated containing the class
name (if found) and the machine address as a unique
identifier. The preference for to_str()
and the
possibility for to_str()
to block calls to
to_string()
allows the class to prioritize a small
representation for print
and
display
while still supporting a
“full” string representation by calling
to_string()
explicitly.to_str()
or to_string()
! An error will
invoke the debugger, which will probably try to print the object
using the same faulty code. Note that if an error occurs in init()
,
objects may not be fully initialized before to_str()
is called. In general,
assume all instance variables might be nil
or some unexpected type. When using instance variable values to
construct a string, consider writing str(instance_var)
even if you expect instance_var
to be a string already,
and if you call a function inside to_str()
, make
sure the parameters are valid. Another strategy, if you have an
elaborate to_string()
method, is to also define a
simpler to_str()
method or just use
def to_str(): nil
, which will allow any stack trace
to use the default representation without calling your code. You
can then invoke .to_string()
when you want
the elaborate string representation, and any errors there will be
reported without further problems.strcat(a, b)
subseq(s, start [, end])
suspend(thread)
symbol_value(s)
tan(x)
system(s)
f.tell()
threadid()
'Thread'
.)thrloc(thread)
time_date()
[
seconds
(0-60), minutes (0-59), hours (0-23),
day-of-the-month
(1-31), month-of-year (0-11), year
(1900-?), day-of-week (Sunday = 0), day-of-year
(0-365), is-summer-time-in-effect?
(t
or nil
),
abbreviation-of-timezone-name (e.g. "EST"
),
offset-from-UTC-in-seconds (positive
values indicate locations east of the Prime
Meridian)]
.f.token()
tolower(s)
totitle(s)
toupper
. Normally, this would be called to convert
individual characters that begin words in a title.toupper(s)
trace(n)
type(object)
s.unappend()
s.uninsert(i [, j])
unlink(filename)
f.unread(c)
a.values()
within(x, y, epsilon)
f.write(str)
str
must be of type Array)
if file is opened in binary mode (see Raw
or Binary File I/O). Returns f
.f.writelines(list)
yield()
yieldto(thread)
iszero(x)
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 symbols (call the corresponding global
function). A planned extension is to let objects represent
functions. When an object is applied to a parameter list, a
special method (possibly 'call') is invoked on the object.
command_line_arguments
serpent64 myprog.srp 15 xyzzy
, then
command_line_arguments
will be initialized to["myprog.srp", "15", "xyzzy"]
. Note that all
elements are strings.
When you run
serpent64
or wxserpent64
from the
command line, you give the name of a Serpent source file
as the first command line argument. (The default if
"init"
.) This can be followed by
additional arguments. The source file name and additional
arguments (but not the Unix command,
e.g. "serpent64"
) are used to initialize
command_line_arguments
. dbg_trace_output_disable
files_loaded
require
statement uses this list to decide whether to load a file.paths_currently_loading
paths_currently_loading.last()
in a statement that is executed while the file is being loaded.runtime_exception
print
and display
commands, but you
can also use file IO commands. The global variable
stdout
is initialized to the terminal, so for
example, stdout.write("Hello World!\n")
is
equivalent to print "Hello World!"
.
There are no special terminal input commands, but you can use
stdin
to refer to the keyboard, so for example,
stdin.readline()
will return a line of
text. Backspace and other editing characters are processed as
you type and are not captured by stdin
. Because of
the built-in editing, you cannot capture individual
keystrokes. Instead, input text becomes readable when you type
the <enter> or <return> key. Consider also
stdin.read(1)
to read the next character only, and
stdin.readvalue()
to read one token at a time,
stripping out white space.
If you develop a lot of code that uses print
and
then decide you would like to redirect the terminal output to a
“real” file, you can reassign stdout
to
a file that is open for writing. Be careful! If you
encounter an error, Serpent error messages and debugging output
will be written to stdout
, so you may not see
it. Also, be sure to save the original value of
stdout
and restore it after redirecting output to a file.
b
”
in the mode parameter, e.g. open("file.raw",
"bw")
or open("file.raw", "br")
. Serpent does
not have a special “blob” or byte-string data type,
and UTF-8 strings are not valid if they contain certain byte
values, so Serpent uses arrays of integers to encode raw binary
byte strings. The read
method
returns an array when the file is opened in binary mode, and the
write
method expects an
array for files opened for binary write.
To use memory more efficiently, each integer encodes
up to 6 bytes, so the byte sequence 1, 2, 3, 4, 5, 6 becomes the
integer 0x060504030201. The first element of the array is the byte
sequence length (as an Integer >= 0). To access the nth
byte of array a
, you can write:
((a[1 + idiv(n, 6)] >> ((n % 6) *8)) &
0xff)
The array methods
get_byte(n)
, set_byte(n, v)
,
append_byte(v)
and append_bytes(a)
are
under consideration as a future extension.
lib
directory, and normally this directory should be on the Serpent
search path. Serpent libraries are evolving with use. Feel free to
contribute new libraries and methods of general utility. The list
below is intended as a rough guide. Please read documentation in the
source files themselves for more detail.
Regression
class can be used to perform
(linear) regression.Statistics
class can be used to perform
simple statistics such as mean and standard deviation. It can
also buffer a set of statistical samples and save them to a
file.String_edit
class is intended to edit files,
e.g. perform global query-and-replace operations. The original
purpose was editing templates to automatically generate HTML as
part of the serpent software release process, but any file or
string editing might make use of this class.String_parse
class is intended to parse data
files of all kinds one line at a time. You pass in a string you
want to parse, and then sequentially advance through the string
skipping spaces, reading words, integers, floats, special
characters, or whatever. Highly recommended for all your text
input needs.irandom
to get random integers, uniform
to get uniformly
distributed real numbers, change_file_suffix
to
replace a file name suffix, file_from_path
to
extract the file name from a full path, and pad
to
pad a string to a given length.To create a thread, use fork()
, which copies the
current stack frame (the current function’s local variables and
program counter). The original thread continues executing and the
new thread starts executing as if both called fork()
.
The difference is that in the original thread, fork()
returns the new thread (a reference to a thread primitive of type
Thread
, while in the new thread, fork()
returns nil
(false
) and when this
thread returns from the function where fork()
was
called, the thread terminates.
After creating a new thread with fork()
, the
calling thread yields to the new thread. You can also suspend
threads to take them off the runnable list so they will not be
yielded to (see suspend()
, and you can make a
suspended thread runnable again (see resume()
).
A typical way to create a thread to perform some computation is:
if not fork(): some_computation() // runs on new thread return ... work for main thread continues ...Alternatively, one can make a function that runs on a separate thread as follows:
def some_computation() if fork(): // create new thread for the work return // caller does none of the work ... some computation here for new thread ...One could also arrange things to defer computation until after the main thread suspends or yields control:
def some_computation() var caller = threadid() // get the calling thread if fork() return // caller will run this soon after fork() // after fork, new thread will run immediately, // so yield control back to calling thread yieldto(caller) // eventually, control will come back to this new thread: ... some defered computation here for new thread ...
proc_create()
,
proc_send()
and proc_receive()
.
All communication between these two procs is through message queues. Two queues are set up and initialized to hold up to 100 strings of up to 100 characters each. Only strings may be sent and received. To build Serpent with these proc functions, link Serpent with the objects obtained from proccreate.cpp and prochack.cpp. (Look for the CMake option USE_PROC to use procs.)
It is strongly recommended that you do not depend heavily on this simple proc interface. It was created to support a course and is not intended for “real” use.
proc_create(period, filename, mempool)
porttime_callback(ms)
,
where ms is the current time in milliseconds.proc_send(proc_id, string)
proc_id
.proc_receive(proc_id)
proc_id)
.server_create(portno)
portno
, and listen
for client connections. A socket descriptor (number) is
returned. -1 is returned to indicate an error.server_accept(socket)
socket
, which was
created by server_create
. If the return value is nil
,
then no client request is pending (this is a non-blocking call).
If the return value is negative, an error occurred. -1 indicates
an error was reported from the accept operation. -2 indicates
that the socket parameter value is invalid. (Other negative
integers should also be treated as errors.) Otherwise, the
return value is socket that can be used to read the client
request. Under Windows, calling this function initiates a
blocking accept
call in another thread. In order
to call server_connect
or socket_receive
,
you must continue (re)calling server_accept
until
it returns something other than nil
. To terminate
the blocked accept
, try closing the server socket
and then re-calling server_accept
to read the
error return.server_connect(name, portno)
nil
if no result
is available yet (this is a non-blocking call), or -1 if there
is an error. If nil
is returned, you must re-call
server_connect
until a non-nil
result
is obtained.socket_receive(socket, n)
n
bytes of data from socket
.
Returns a string if successful, nil
if no input is
available (this is a non-blocking call), and otherwise returns
an integer error code. The error code -2 is returned if the
socket parameter is invalid. The socket is normally obtained
from server_accept
or server_connect
.
If nil
is returned, the read is still in progress,
and you must re-call socket_receive
until a non-nil
result is obtained.socket_send(socket, string)
server_accept
or server_connect
.
Returns the number of bytes sent or -1 on error.socket_close(socket)
sfo_copy_directory(from_path, to_path)
from_path
to to_path
(both arguments are strings).sfo_delete(path)
path
(a
string).create_directory(path)
path
(a string).local_time()
Unless otherwise specified, O2 functions return an integer error
code which is O2_SUCCESS
(= 0) if no errors were
detected. The integer can be converted to a readable string using
o2_error_to_string()
.
The functions are:
o2_debug_flags(flags)
flags
are a string. Options are: (c)
basic connection data, (r) non-system incoming messages, (s)
non-system outgoing messages, (R) system incoming messages, (S)
system outgoing messages, (k) clock synchronization protocol,
(d) discovery messages, (h) hub-related activity,
(t) user messages dispatched from
schedulers, (T) system messages dispatched from schedulers,
(l) trace (local) messages to o2_message_deliver
(m)
memory allocation and free, (o) socket creation and closing, (O)
open sound control messages, (q) show MQTT messages,
(g) general status info, (n) all network flags: rRsS, (a) all
debug flags except m (malloc/free), (A) all debug flags except
malloc and scheduling, (N) disable network if flags are set
before o2_initialize()
is called. Internal IP
becomes 127.0.0.1, public IP is 0.0.0.0 (signifying no Internet
connection). Interprocess communication on the host is
supported.o2_network_enable(flag)
o2_initialize()
. Default is network
enabled. See also o2_debug_flags("N")
.o2_initialize(application, debug_flag)
application
is a unique string
shared by all O2 processes in a distributed O2 application. (You
can have multiple O2 applications sharing the same local area
network as long as each application uses a different name.) If debug_flag
is non-nil, debugging information may be printed. (Exactly what
is printed is intentionally not specified.)o2_get_addresses()
o2_service_new(service)
service
, a string
parameter.o2_get_proc_name()
o2_services_list()
o2_services_list_free()
to release the data structure (or
you can simply leave it – the old snapshot will be freed the
next time you call o2_services_list
).o2_services_list_free()
o2_service_name(i)
o2_service_type(i)
o2_service_process(i)
o2_service_tapper(i)
o2_service_properties(i)
o2_service_getprop(i, attr)
o2_service_search(i, attr, pattern)
o2_service_set_property(service, attr, value)
o2_service_property_free(service, attr)
o2_tap(tappee, tapper, send_mode)
tappee
.
Messages to tappee
are copied and send to tapper
. Both tappee
and tapper parameters are strings. The send_mode
parameter is one of 0 for TAP_KEEP, 1 for TAP_RELIABLE, or 2
for TAP_BEST_EFFFORT. This parameter controls the send mode when
messages are tapped. TAP_KEEP means the message is sent to the
tapper using the same method (TCP or UDP) as that of the message
to the tappee.o2_untap(tappee, tapper)
o2_service_free(service)
service
, a string
parameter.o2_method_new(path, types, method, coerce)
path
, a
string (e.g. "/myservice/slider"
). To accept
any incoming address to a service, use the service name
for path
, e.g. "/myservice"
. In
addition to matching path
, message types can be
checked, and messages that fail the type checks are rejected
(dropped). Type checking has three levels: If
types
is nil
, no type checking is
performed, and all messages matching path
are
accepted. The method
is called with: the message
timestamp, the message address, the actual type string from
the message, and with one argument for each data element in
the message. (The type string is not redundant because
multiple O2 types such as 'f'
loat and
'd'
ouble are converted to the same Serpent type
(Real
.) If types
is not
nil
, it must be a string of O2 type
characters. If coerce
is true, then the actual
message data elements are coerced to the types given by
types
, if possible, and the message is delivered
by calling method
. If coercion is not possible,
the message is dropped. Finally, if coerce
is
false, the message types must match types
exactly, in which case method
is called with the
expected types (saving any further type checking in Serpent);
otherwise, the message is dropped. Note that if
types
is nil
(or equivalently,
false
), you can receive messages of varying
numbers of parameters by using the rest
keyword
in the method
parameter list to assemble the
multiple parameters into a Serpent array. The parameters
passed to the method are timestamp, the O2 message
timestamp, address, the address string from the
message, types, the type string for the actual
parameters, and finally the actual parameters extracted from
the message. The types will match types.o2_method_free(path)
o2_message_warnings(fnsymbol)
o2_poll()
o2_poll()
,
e.g. call time_sleep(0.001)
. Reasonable polling
periods range from 1ms to 100ms. Timed message delivery happens
during o2_poll()
, so timing precision is directly
related to the polling period.o2_status(service)
service
,
a string parameter. The return value is an integer code as
follows:
service
is
null, empty, or contains a slash (/) or exclamation point
(!). o2_can_send(service)
o2_roundtrip()
o2_clock_set()
o2_clock_jump(local_time, global_time, adjust)
o2_sched_flush()
Scheduler
or Vscheduler
objects.o2_time_jump_callback
o2_clock_jump
. Otherwise, the time jump simply
happens when the call returns or if
o2_time_jump_callback
is left undefined.
o2_time_get()
o2_local_time()
o2_error_to_string(error)
error
into a
human-readable string, which is the return value.o2_finish()
o2_osc_port_new(service, port, tcp_flag)
service
. The service
is offered on the port given by the integer port
,
and the port will receive messages via UDP unless tcp_flag
is non-nil, in which case TCP is used.o2_osc_port_free(port)
port
, but
do not free the service that it forwarded to. Call
o2_osc_delegate(service, ip, port, tcp_flag)
service
,
that forwards O2 messages to an OSC server defined by the string
ip
(IP address or "localhost"
), the
integer port
(port number), and the boolean tcp_flag
which specifies whether to connect via UDP or TCP.o2_send_start()
o2_send_finish()
. Other sequences of calls
to O2 result in undefined behavior. Calls to append parameters
are as follows:
o2_add_int32(i)
i
to the
current message initiated by o2_send_start()
.o2_add_float(x)
x
to the current message
initiated by o2_send_start()
.o2_add_blob(s)
s
.o2_add_string(s)
s
to the current
message initiated by o2_send_start()
.o2_add_int64(i)
i
to the
current message initiated by o2_send_start()
. Note
that Serpent64 integers are 50-bit signed integers, so using all
64 bits is not possible.o2_add_double(x)
x
to the current
message initiated by o2_send_start()
).o2_add_time(t)
t
to the current
message initiated by o2_send_start()
. Note that O2
times differ from OSC timetags even though both use the
character "t" as the type code. Although O2 might convert
between these representations, do not expect meaningful
information to flow between O2 and OSC through type "t"
arguments.o2_add_symbol(s)
s
to the current
message initiated by o2_send_start()
.o2_add_char(c)
c
to the current message
initiated by o2_send_start()
.o2_add_midi(cmd, d1, d2)
o2_send_start()
.
Within the O2 message, the message has the OSC representation of
a 32-bit int containing (from high- to low-order bytes) port,
status, data1 and data2 bytes. The port (not used) is zero.o2_add_bool(b)
b
to the current
message.o2_add_nil()
o2_send_start()
.
The value passed as actual argument to the message handler is
Serpent's nil
.o2_add_infinitum()
o2_send_start()
. The value passed as
actual argument to the message handler is the symbol o2_add_start_array()
o2_send_start()
.
This is represented in the type string with the character "[".o2_add_end_array()
o2_send_start()
.
This is represented in the type string with the character "]".
The actual argument delivered to the handler will be a Serpent
array. Nested arrays are supported. You might expect o2_add_array(a)
so simply take a Serpent array and add the elements to a
message. This is not provided because the types of the elements
would be ambiguous (e.g. whether to send float
vs.
double
cannot be determined from Serpent types
alone.) However, o2_add_vector()
can add
homogeneous arrays of numbers.o2_add_vector(v, type)
type
which is a string argument
containing "i"
, "h"
, "f"
,
or "d"
for int32, int64, float, or double.o2_send_finish(time, address, tcp_flag)
o2_send_start()
with timestamp time to the given address
, using
TCP if tcp_flag
is non-nil, and otherwise UDP.
(However, messages within a process are delivered directly
without any network transmission.)o2lite_initialize()
o2_http_initialize(port, root)
ow2s.js
for browser-side implementation. The
port
is the server port number (an integer) and
root
is prefixed to the URL to form a path to web
pages. It can be relative (to the Serpent current directory) or absolute.o2_mqtt_enable(server, port)
osc_server_init(port, debug_flag)
port
string (e.g. "7770"). If debug_flag
is non-nil,
debugging information may be printed. (Exactly what is printed
is intentionally not specified.) It is an error to open more than one OSC server.
On success, OSC_SERVER_SUCCESS
(= 0) is returned.
Upon failure, an integer error code less than zero is returned.
If the OSC server is already initialized and this call is made
from the same thread as the initial one, OSC_SERVER_ALREADY_OPEN
is returned. However, if an attempt is made to initialize the
server from a second thread, an error is raised, stopping
execution of the thread and calling the debugger or resetting
Serpent to the top level command prompt. The port
string may also be the special string "reply"
, in
which case the server will listen to the reply port, which was
previously set by a call to osc_send_reply()
or
automatically generated by the first call to osc_send()
,
whichever was first.osc_server_port(port)
osc_server_multicast_init(group, port, debug_flag)
group
and port
parameters are strings. Otherwise, behavior is identical to osc_server_init
described above. osc_server_method(path, types, obj, method)
path
,
a string (e.g. "/slider"). The types of the arguments are
given by types
, a string (e.g. "if"). When a
message matching path
is received, arguments are
coerced into the specified types and a handler specified by method
is called. If obj
is non-nil, it must be an Object
with the given method.
Otherwise, method
is called as a global function. The first method
parameter will receive the (string) value of the path
parameter. The remaining method
parameters must be
compatible with types
(e.g. either the count
matches or method
has optional
or rest
parameters).osc_server_recv_port()
method
is called,
the port from which the message was received can be retrieved
with this function. The return value is an integer. The result
is only valid if the function is called from within the method
that is handling an incoming message.osc_server_sender_ip()
method
is called,
the message sender's IP address can be retrieved with this
function. The return value is a string in the form
nnn.nnn.nnn.nnn. The result is only valid if the function is
called from within the method that is handling an incoming
message.osc_server_poll()
osc_server_method.
Normally, 0 is
returned, but upon failure, an integer error code less than zero
is returned. You should call this function at a rate consistent
with the rate of OSC messages and your tolerance for latency.osc_server_reply()
osc_server_method()
. Within
this method, it is possible to access the reply address of the
incoming message by calling osc_server_reply()
.
There are no parameters, and the result is an integer. If the
integer is non-negative, it is an address identifier, compatible
with results from osc_create_address()
and osc_send()
.
Otherwise, an error occurred and no address was found or
created. The reason you might want to use a reply address,
rather than, say, an agreed-upon IP address and port number, is
that if the client is behind a router that uses NAT, the router
might change the client's IP address and port number. In this
scheme, called Net Address Translation (NAT), the only way to
get a message back to the client is to reply. The router will
then do the reverse translation on the address to reach the
client. osc_server_reply
does nothing and returns
-1 in the Liblo version.osc_server_finish()
osc_create_address(host, port, bind)
host
and port
strings. If host
is nil
,
the host is the local host. If bind
is true, a
bind is performed on the created socket. If this is the first
created address and osc_server_init()
has not been
called, then the OSC server is initialized with this port. This
allows the socket to receive messages, and as a side-effect, the
port will be added as a reply port to outgoing messages. (See osc_server_reply
()
above.)
The bind
parameter is ignored in the Liblo
version. Returns an address identifier (a non-negative integer)
on success, or a negative integer error code on failure. Note:
Serpent can manage only a limited number of addresses, so create
an address once and save it for use with osc_send
.
Do not call osc_create_address
every time
you call osc_send
. osc_delete_address(n)
n
. The
implementation has room for a finite number of addresses, so if
osc_server_reply()
is used to get client reply
ports and clients reconnect often, the server should try to
remove stale reply ports to avoid filling up the address table.osc_send_reply(port)
osc_send_reply()
, where port
is a string number in decimal, such as "7771"
. If
this call is not made, a reply port is chosen arbitrarily. After
calling osc_send_reply
or osc_send
,
osc_server_poll()
can be called to process reply
messages. (Do not call osc_server_init
).
Returns zero on success, negative number on error. osc_send_reply
does nothing and returns -1 in the Liblo version.osc_send_start()
osc_send_start()
a second time with no
intervening call to osc_send
.osc_add_double(x)
x
to the current
message (which must have been created by osc_send_start()
).osc_add_float(x)
x
to the current message
(which must have been created by osc_send_start()
).osc_add_int32(i)
i
to the
current message.osc_add_int64(i)
i
to the
current message.osc_add_string(s)
s
to the current
message.osc_add_timetag(i)
i
to
the current message.osc_add_symbol(s)
s
to the current
message.osc_add_char(c)
c
to the current message.osc_add_midi(cmd, d1, d2)
osc_add_bool(b)
b
to the current
message.osc_add_nil()
osc_add_infinitum()
osc_send(address, path)
address
, a
value returned by path
, a string. You must call osc_client_start()
and append arguments to send another message. A return value
of -1 indicates that the address parameter is invalid. This API is intentionally small and simple. If users find OSC
useful and need additional features, feel free to contact the
author about extending this specification. Also, any serious OSC
client should probably write a Serpent library to at least
implement something like osc_send(path, types, arg1, arg2,
...)
to construct and send an OSC message.
ZeroMQ is a message passing library and concurrency framework. When Serpent is linked with ZeroMQ, the following functions are available.
zmq_init()
zmq_open_reply()
zmq_open_request()
zmq_open_publish()
zmq_set_filter
). ZeroMQ assumes the socket is
send-only. Null is returned on error, otherwise a socket is
returned. You should next connect or bind the socket.zmq_open_subscribe()
zmq_open_push()
zmq_open_pull()
zmq_bind(socket, protocol, host, port)
zmq_bind
,
but one side must use bind, and the other end must use connect.
The protocol choices are the strings "tcp", "ipc", or "inproc"
(see ZeroMQ documentation for more information on protocols).
The host is a string, and the port is an integer. For "tcp," the
host is typically "*." For "ipc" the "host" is really the
address, e.g. "/tmp/feeds/0", and port is ignored. For "inproc,"
"host" is really a name, e.g. "myendpoint" and port is
ignored. Returns success (true) or failure (false). For
"inproc", the bind must take place before a socket is connected.zmq_connect(socket, protocol, host, port)
zmq_subscribe(socket, filter)
zmq_unsubscribe(socket, filter)
zmq_subscribe()
.zmq_send(socket, message)
zmq_recv_noblock(socket)
zmq_recv_block(socket)
zmq_close(socket)
zmq_term()
Serpent has an extended syntax to support Aura message passing. This syntax is enabled by setting the AURA flag during compilation. Normally, you would only do this when compiling Serpent as a library for linking with Aura, but you can also compile a stand-along Serpent this way.
An Aura message is used to invoke operations on Aura objects.
These are "real" messages is the sense that they are represented
as a sequence of bytes and they can be sent to objects in other
threads or even to other address spaces. Aura messsages have a
method identifier (a string) including a type signature and a
set of parameters. The type signature for a given method
identifier is the same for all objects. For example, the method
identifier "set_hz
" has one parameter that is a
double. All objects that accept the "set_hz
" have
the same signature and require one double.
To send an Aura message, you can use one of the following syntax forms:
targetWhile this looks like some kind of procedure call, it is quite different. The<-
message(
p1, p2, p3)
target<-
message(
p1, p2, p3) @
when
<-
message(
p1, p2, p3)
<-
message(
p1, p2, p3) @
when
target
, which is optional, is any
expression that evaluates to an Aura ID (represented in Serpent as
an integer, and usually obtained by a call to the function aura_create_inst
,
loaded from auramsgs.srp
.)
The message
must be an identifier that
has been declared as an Aura message. Although parsed as an
identifier (unquoted), Serpent uses the string name of this
identifier to form the method part of an Aura message. The message
is also used to find the type signature for the message by using
message
(now as a symbol) as a key in the Serpent
dictionary aura_type_strings
. The lookup must
succeed at compile time or an error is reported. The result of
the lookup is a string consisting of the letters "i" (Long), "d"
(Double), "s" (String), "l" (Logical), and "v" (Vector of
floats, obtained from a Serpent array).
The parameters p1
through p3
are a
comma-separated list of expressions equal in number to the
length of the type string for message
. The types
of these parameters must be compatible with the letters of the
type string, but full checking can only be done at run time.
The Aura Send statement is compiled to a call to aura_zone_send_from_to
.
Normally this is a built-in function that calls into Aura to
construct and send a message. The parameters to aura_zone_send_from_to
are:
aura_zone_id
-- The Aura ID for the current
zone. This is obtained from the global variable aura_zone_id
,
which must have a value at run time. aura
-- The Aura ID of the sending object. This
is 0 if there is a target
expression and the
message is actually sent from the current zone using aura_zone_id
as the sender. If there is no target
, then the
message is sent to all objects the sender is connected to. In
this case, the sender's Aura ID must be in the variable "aura
."
Normally, the aura send statement would be in the method of a
class that inherits from class Aura
, which
provides an instance variable named aura
. destination
-- The Aura ID for the receiver. If
a target
expression appears, the destination is
the value of that expression. Otherwise, destination
is 0 and the message is broadcast to all connected receivers.
method
-- The string name of the method to be
invoked, obtained from message
. parameters
-- An array consisting of the type
string for method
followed by parameter values,
obtained by evaluating the parameter expressions. The array is
automatically allocated and constructed. timestamp
-- A timestamp giving the Aura time
for the message to be delivered. (Aura messages are held in a
queue until their delivery time.) The default value is the
value of the variable AURA_NOW
which must be
defined (normally to 1e-9) at runtime. Note that the Aura preprocessor should be used to generate
correct type strings. Do not add type strings to aura_type_strings
manually.
For testing, it is possible to compile Serpent using the AURA
compile-time flag. You will have to define things expected by
the compiler including aura_zone_send_from_to
, but
this can be an ordinary Serpent function.
This creates an "Iecho" instance, sets the input to the left channel (0) of the system audio input, and sets the delay to 0.3 seconds. The last (third) statement is equivalent to the following:iecho_aura = aura_create_inst("Iecho", 3)
iecho_aura <- set_in(audio_io_aura, 0)
iecho_aura <- set_delay(0.3)
aura_zone_send_from_to(aura_zone_id, 0,
iecho_aura,
"set_delay",
["d",
0.3], AURA_NOW)
interface.srp
.
Not all C types are supported, and the mapping between Serpent and
C types has some restrictions and special cases, so sometimes the
developer must create some "glue" functions to translate between
Serpent and C. The supported types are as follows:
Type name in interface description | Converted to/from this type | Type within Serpent |
long | int64 | Integer |
short | int64 | Integer |
int | int64 | Integer |
char | char | String |
string | char * | String |
double | double | Real |
float | double | Real |
bool | SVal | Symbol |
any | SVal | -- |
Object_ptr | Object_ptr | Object |
FILE * | FILE * | File |
/*SER type function_name [c_name] (type1, type2, type3, ...) PENT*/
Generates an interface tofunction_name
, which refers to the Serpent name for the function. If the C name is different, it is specified between square brackets. (Note that bracket characters actually appear in the comment; they are not meta-syntax characters.) The types are parameter types as shown in the first column of the table of types shown above. In addition, "external" types may be specified as extern typename.Finally, there are cases where the function should have access to the virtual machine, which is a C++ object of type
Machine
. Serpent programs cannot access the virtual machine as an object, so it is impossible to explicitly pass the machine as a parameter. However, if the type is specified as Machine, a pointer to the machine of the caller will be passed automatically. Since the parameter is implicit, the generated function in Serpent will have one less parameter than the corresponding C function.
/*SER class Class_name PENT*/
Serpent can be extended with new types using this form of comment.
/*SER variable = value PENT*/
Global variables in Serpent can be initialized to a
value using this form of comment. The value must be a string,
integer, symbol, or real constant. No expressions are allowed.
When in doubt, value
is converted to
symbol.
extclass.cpp
give an
example of an interface to a class. Note the use of the class
Descriptor to describe the external class to the Serpent run-time
system. Descriptor
is subclassed to build a
descriptor for the new type. This is not an automatic operation;
you must build your own descriptor subclass.
extfuncdemo.cpp
provide
an example of an interface to ordinary functions written in C or
C++.
interf
function as
shown in the following example:
interf("smid", ["midi.h", "midiserpent.h"])
The first parameter specifies the output file name (without the
.cpp extension). This name is also used for some internal names
that must be generated. The second parameter is a list of files to
process. Each of these files is included in the generated output
file using an #include directive, so if you want a file
included, list it even if it contains no /*SER ... PENT*/
comments.
In order to find header files not in the current directory, you can provide a search path as follows:
interface_search_path = ["..\\midi\\", ...]
The Serpent sources include syntax-aware editing mode support for
Sublime and Emacs. See serpent/extras/README.txt
,
which you can find either in the installed source code (if you
download all of it), or get the specific files you need by browsing
the
Serpent code hosted on sourceforge.net.