Vous êtes sur la page 1sur 15

GenExpr

docs.cycling74.com/max6/dynamic/c74_docs.html

GenExpr is the internal language used by gen patchers. It is used to describe


computations in an implementation agnostic manner. To perform actual computations, it
is translated into machine code for the CPU or GPU by the various Gen objects ( gen~ ,
jit.gen , etc.).
The GenExpr language can be used directly in gen patchers with the expr and codebox
objects. These objects analyze the expressions written in them and automatically
construct the the appropriate number of inlets and outlets so that patchcords can be
connected to the computations described within.

Note that there is absolutely no difference in terms of performance between describing


computations with object boxes and the GenExpr language. When a gen patcher is
compiled, it all gets merged into a single representation, so use the approach that is most
convenient for the problem.

The Gen Patcher and the codebox Object


The GenExpr language is designed to complement the Max patching environment within
Gen patchers. It provides a parallel textual mechanism for describing computations to be
used in concert with the graphical patching paradigm of Max. As one example, the
structural elements of user-defined GenExpr functions correspond closely to the
structural elements of Max objects with their notions of inlets, outlets, arguments and
attributes. Furthermore, the GenExpr language has keywords in, in1, in2, … and out,
out1, out2, … that specifically refer to the inlets and outlets of the expr or codebox the
GenExpr code is embedded in.

Adding a codebox to a Patch and Compiling It


Click in a blank space in your unlocked patcher window and type "n" (new) to
create a new object box with a cursor. Type in the word 'codebox,' followed by a

1/15
return. When you hit return, a new codebox will be created.

Enter your GenExpr language code into the codebox . When you're done, click on
the Compile button at the bottom of the codebox window to compile and run your
code.

Language Basics
The GenExpr language’s syntax resembles that of C and JavaScript for simple
expression statements like those in the codebox above, however, semicolons are only
necessary when there are multiple statements. The codeboxes below all contain valid
expressions in GenExpr. When there is a single expression with no assignment like in
the far left codebox, the assignment to out1 is implied. Notice that it also doesn’t have a
semicolon at the end. When there is only one statement, the semicolon is also implied.

2/15
For multi-line statements however, semicolons are required. The codebox on the left
doesn’t have them and will generate errors. The codebox on the right is correct.

The expr operator is functionally the same as a codebox but lacks the text editor features
such as syntax highlighting and multi-line text display and navigation. expr is most useful
for short, one-line expressions, saving the effort of patching together a sequence of
objects together that operate as a unit.

An expr or codebox determines its number of inlets and outlets by detecting the inN and
outN keywords where N is the inlet/outlet position. in1 and out1 are the left-most inlet and
outlet respectively. For convenience, the keywords in and out are equivalent to in1 and
out1 respectively.

3/15
Almost every object that can be created in a Gen patcher is also available from within
GenExpr as either a function, a global variable, a declaration, or a constant. The number
of inlets an object has corresponds to the number of arguments a function takes. For
example, the object atan2 has two inlets and takes two arguments as follows:

out = atan2(in1, in2)

Parameters
Parameters declared in GenExpr behave just like param operators in a patch. They can
only be declared in the main body of GenExpr code where inlets and outlets (inN and
outN) exist because they operate at the same level as object box Gen operators in the
patcher.

A Param declaration in GenExpr names a parameter and creates it if necessary. If there


is a param object box with the same name as a Param declared in GenExpr, the
GenExpr Param will simply alias the object box param. If there isn't a param object box
with the same name, one will be implicitly created. In the code above, offset aliases the
object box param offset, while amp creates a new global param.

Comments
Comments in GenExpr follow the C style syntax for both single-line and multi-line
comments. Single-line comments start with // and multi-line comments are defined by /*
until the next */.

// this is a comment, below we sample an input


pix = sample(in1, norm);

4/15
Multiple Return Values
Just as object boxes can have multiple inlets and outlets, function in GenExpr can take
multiple arguments and can return multiple values. The object cartopol has two inlets and
two outlets. Similarly, in GenExpr the cartopol function takes two arguments and returns
two values. In code, this looks like r, theta = cartopol(x, y). Functions that return mutiple
values can assign to a list of variables. The syntax follows the pattern: var1, var2,
var3, … =

When a function returns multiple values but assigns to only one value, the unused return
values are simply ignored. When a return value is ignored, the GenExpr compiler
eliminates any unnecessary calculations. The function cartopol could be expanded out to

r, theta = sqrt(x*x+y*y), atan2(y, x)

If we remove theta and have instead

r = sqrt(x*x+y*y), atan2(y, x)

the compiler simplifies it to

r = sqrt(x*x+y*y)

In the reverse case where we only use theta, the Gen compiler will strip out the
calculations for r

notused, theta = sqrt(x*x+y*y), atan2(y, x);


out = theta;

effectively becomes

theta = atan2(y, x);


out = theta;

Even for more complex examples where the outputs share intermediate calculations, the
compiler eliminates unnecessary operations, so there is no performance penalty for not
using all of a function’s return values.

Just as the left-hand side list of variable names being assigned to are separated by
commas, the right-hand side list of expressions can also be separated by commas:

5/15
sum, diff = in1+in2, in1-in2
out1, out2 = diff, sum

If there are more values on the left-hand side than on the right-hand side, the extra
variable names are given a value of zero.

For example,

out1, out2 = in1

becomes

out1, out2 = in1, 0

If any of the expressions in the right-hand side return more than one value, these
additional values will be ignored unless the expression is the last item in the right-hand
side list. This is complex to describe, but should be clear from these examples:

Unused Return Values

The second return value gets discarded and cartopol is optimized:

r = cartopol(x, y)

Extra Assignment Values

Zeros are assigned to extra assignment values:

x, y = in1

becomes

x, y = in1, 0

Multiple Return Values in an Expression List

Only the last expression can return multiple values. cartopol’s second return value
discarded, as it is not the last expression in the right-hand side:

r, out1 = cartopol(x, y), in1

6/15
Here cartopol returns both values, since it is in the last position:

out1, r, theta = in1, cartopol(x, y)

The same principle applies when expressions are used as arguments to a function call.
In this example, the two output values of poltocar connect to the two input values of min:

out = min(poltocar(in1, in2))

Defining GenExpr Functions


Defining new functions in GenExpr happens in much the same way as other familiar
programming languages. Since there are no types in GenExpr function arguments are
specified simply with a name. A basic function definition with an equivalent patcher
representation looks like:

A function returning multiple values looks like:

7/15
The cylinder operator in Jitter Gen objects is defined as:

8/15
While simple functions in GenExpr can be easily patched together, more involved
functions like the above cylinder definition start to become unwieldy, especially if the
function is used several times within the GenExpr code. This is the advantage of textual
representations.

Operator Attributes

In Gen patchers, some objects have attributes like the Jitter Gen operator sample, which
as a boundmode attribute. In GenExpr, function arguments correspond to operator inlets
and function return values correspond to outlets. Attributes are set using a key/value style
argument. For example:

out = sample(in, norm, boundmode="mirror");

will use a version of the sample operator with a mirroring boundary behavior. The
attribute is set with boundmode as the key and "mirror" as its value. Since the concept of
Max messages doesn't exist within Gen patchers, attributes for built-in operators are not
dynamically modifiable. This holds equally in GenExpr. Such attribute values must be
constants. If the attribute takes a numerical value, it cannot be assigned to from a
variable.

Attributes in Function Definitions

Attributes can also be defined for function definitions. Here, attributes can be dynamic,
behaving in a similar manner to how setparam interacts with subpatcher parameters.
Attributes can be defined in one of two ways. With one syntax, the attribute is defined and
given a default in the function signature. With the other, a Param object is declared in the
function, adding the name of the parameter as an attribute to the function.

9/15
With the first method, only the default value of the attribute can be defined. With the
second method, other properties such as minimum and maximum values for the attribute
can be set. By declaring a Param object, you get more control over how the attribute
operates.

As with built-in operators, attributes of function definitions can be set with key-value
syntax. In the above example, the amp attribute is given a value of 0.5 while the offset
attribute is given a value of in1*2, which is an expression that is not constant but valid
because func is a function definition. Note, however, that the offset attribute has
minimum and maximum values defined, so any expression assigned to it will be clamped
to that range.

Abstractions as GenExpr Functions


Structurally, GenExpr functions are equivalent to Gen patchers. Both can have inputs,
outputs and named parameters. In GenExpr, Gen patchers save as abstractions
(.gendsp or .genjit files) can be used as functions. When the GenExpr interpreter

10/15
encounters a function it can't find the definition of, it will use the current Max search paths
to look for a Gen abstraction with the name of the function.

For example, if I have the code:

out1 = myAbstraction(in1)

There is no definition of the function myAbstraction in the above code and it isn't a built-
in operator. As a final attempt to define myAbstraction, the GenExpr interpreter will look
for myAbstraction.gendsp in the case of gen~ or myAbstraction.genjit in the case of the
Jitter Gen objects.

There are some caveats with using abstrasctions as GenExpr functions. GenExpr
function names must be valid identifiers. An identifier in GenExpr is a sequence of
characters starting with a letter or an underscore ([a-z], [A-Z], _) followed by any number
of letters, numbers or underscores ([a-z], [A-Z], [0-9], _). It is not uncommon for Max
subpatchers to have other chartacters such as '~' or '.' in them. These are invalid
characters when it comes to GenExpr function names, so if they're used in the name of a
Gen abstraction, they cannot be used as GenExpr functions.

Requiring GenExpr Files


When defining operators in GenExpr, it can be handy to keep them in a separate file so
that they can be reused frequently without having to constantly re-type the operator
definition in a codebox. To include GenExpr operators defined in a separate file, use the
require operator. The require operator takes the name of a .genexpr file and loads its
definitions. The following are all valid ways to require a .genexpr file:

require("mylib")
require("mylib");
require"mylib"
require"mylib";

In the above code, calling require triggers the codebox to look for the file 'mylib.genexpr'
using the Max search paths. Required .genexpr files can also require other files. If a file is
required more than once, it will only be loaded once.

GenExpr files can be created and edited using the built-in Max text editor. If a GenExpr
file is required in a gen object and the file is edited and saved, the gen object will detect
that a file it depends on has changed through filewatching and recompile itself to reflect
the new changes.

Branching and Looping

11/15
Branching and looping is supported in GenExpr with if/else, for and while constructs. if
statements can be chained together using else if an arbitrary number of times such that
different blocks of code will be executed depending on different conditions. For example:

if(in > 0.5) {


out = cos(in*pi);
}
else if(in > 0.25) {
out = sin(in*pi);
}
else {
out = cos(in*pi)*sin(in*pi);
}

Note that in the Jitter gen objects, if statements with vector-valued conditions will only
use the first element of the vector to determine whether a block of code should be tested
or not.

while loops in GenExpr are similar to those in many other languages:

i = 0;
val = 0;
while(i < 10) {
i += 1;
// accumulate
val += i;
}
out = val;

for loops are also similar to this in many other languages although there is no ++
operator in GenExpr to increment a loop counter. Instead, += can be used:

val = 0;
for(i=0; i < 10; i += 1) {
// accumulate
val += i;
}
out = val;

Since GenExpr is compiled on-the-fly, it can be easy to make a programming mistake


and create an infinite loop. All of the gen objects have protections against infinite loops,
so while an infinite loop might slow things down, it won't cause Max to get stuck and
freeze.

Also, note that in many cases values in GenExpr are floating point, even loop counters.
Floating point numbers can't exactly represent every number, sometimes a little fudge
factor to account for this might be necessary. For example, this loop:

12/15
val = 0;
for(i=0; i <= 1; i += 0.05) {
// accumulate
val += i;
}
out = val;

will likely not reach 1.0 despite the <= operator because of floating point precision.
Instead, write something like:

val = 0;
for(i=0; i <= 1.04; i += 0.05) {
// accumulate
val += i;
}
out = val;

to ensure that the i variable goes all the way to 1.

Continue and Break


With looping constructs, GenExpr supports break and continue statements. break causes
an early exit from a loop while continue causes the loop to start the next iteration without
finishing the current one.

val = 0;
for(i=0; i < 10; i += 1) {
if(val > 20) {
// bail early
break;
}
val += i;
}
out = val;

val = 0;
for(i=0; i < 10; i += 1) {
if(val == 6) {
// skip an iteration
continue;
}
val += i;
}
out = val;

GenExpr and Jitter Inputs


Jitter Gen objects take both Jitter matrices (jit.matrix) and/or textures (jit.gl.texture)
depending on the object. Within the Gen patcher the operator in represents both the
13/15
input matrix or texture in its entirety and the current cell of that input being processed. In
most cases, the in operator represents the current cell being processed. The only time
where it represents the entire input is with the sample and nearest operators. Only an in
operator can be connected to the left input of sample and nearest, which are used to
grab data from arbitrary locations within the input. The same holds true when sample and
nearest are used in GenExpr.

When GenExpr code is compiled, the inputs to sample and nearest are validated to
ensure that their first arguments are actually Gen patcher inputs and an error thrown if
this isn't the case. The validation process can track inputs even through function calls so
sample and nearest can be used within functions without issue.

GenExpr and Jitter Coordinate Operations


The coordinate operations in Jitter Gen patchers (norm, snorm, cell, and dim) are
special-case operators that are a hybrid betwen operator and global variable. There are
two equally valid syntaxes for using these operators:

out1 = norm * dim();

In the first instance above, norm is syntactically a global variable while dim is syntactically
a function call. All of the coordinate operators follow this convention.

14/15
Technical Notes
GenExpr is a type-less language. Variables are given types automatically by the compiler
depending on the Gen domain and the Gen object’s inputs. Gen variables are also local-
to-scope by default so they don’t have to be declared with a keyword like var as in
JavaScript. Note that GenExpr has no array notation [index] as there is currently no
notion of an array structure.

15/15

Vous aimerez peut-être aussi