Skip to content

Language Specification

Keenan Crane edited this page Feb 14, 2023 · 9 revisions

This page documents features of Penrose Domain schemas and the Substance and Style languages not written up in the SIGGRAPH '20 paper, as well as changes made to these languages since the publication of our paper.

Domain schemas

Subtype relationships can now be expressed using the following syntax:

type T
type S <: T -- declares that S is a subtype of T

The constructor syntax has been changed; it now looks like this:

constructor MyConstructor( Type1 x1, Type2 x2, ... ) -> ConstructedType

The Style language

Strings

Strings have type string and string literals are delimited by double quotes. Strings can be concatenated using the + operator. For instance, to put parentheses around the label associated of x, write

string fancyLabel = “(“ + x.label + “)”

Local variables

A local variable is not attached to a Substance object, and so cannot be referred to by any other block that brings a Substance object in scope. Variables only live in the scope of the block and can have names reused in following blocks.

Within the scope of a block, local variables are immutable references (like const in JS) and can't be redefined.

Local variables can refer to anything in scope, e.g. things in other namespaces. Example:

Colors {
    color red = rgba(1, 0, 0, 1) —- local var in namespace
}

forall Set X {
    scalar pi = 3.14 —- local var in block
    scalar pi = 2 * pi —- yields a compiler error

    color myRed = lighten(Colors.red) // this is fine

    scalar X.x = 2
    scalar X.x = 3 —- this assignment is valid, since fields and properties are mutable references

    shape X.shape = Circle {
        x: X.x
        r: pi * 100
        color: myRed
    }
}

forall Set `A` {
    —- `A`.pi does not exist, so it doesn't make sense to write "override `A`.pi = ..."
    scalar pi = 6.28; —- can define its own
    ...

    override `A`.shape.r = pi * 100 —- Yields a different value
}

Vectors and matrices

Style supports dense n-dimensional vector and matrix types, and standard operations on these types. These types behave largely like small, dense matrix types found in other languages (such as GLSL), with some specific differences noted below. Note that these types are meant largely for manipulating small 2D, 3D, and 4D vectors/matrices in the context of standard graphics operations (transformations, perspective projection, etc.), and may not perform well for larger matrix manipulations. Like all other objects in Style, the value of any vector or matrix entry can be declared as unknown (?) and determined automatically via optimization by the layout engine.

Vector and matrix types

Style is designed to support n-dimensional dense vectors of type vecN, and square n-dimensional matrices of type matNxN, where in both cases N is an integer greater than or equal to 2. E.g., types commonly used for diagramming are vec2 and mat3x3. The compiler may or may not accept other declarations (e.g., rectangular matrices); however, behavior on these types is currently undefined and unsupported. Some library functions may be available only for vectors or matrices of a specific size (e.g., the function cross(u,v), which computes a 3D cross product, assumes that both u and v have type vec3).

Initializing vectors and matrices

A vector is constructed by specifying its components. For instance,

vec2 u = (1.23,4.56)

constructs a 2-dimension vector with x-component 1.23 and y-component 4.56. As noted above, unknown values can be used as components, e.g.,

vec2 p = (?, 0.0)

specifies a point p that sits on the x-axis, with an undetermined x-coordinate. This coordinate will be determined by the optimizer, according to any constraints and objectives involving p. More advanced initializers (e.g., initializing a 3-vector from a 2-vector and a scalar) are currently not supported, but are planned for future language versions. In most cases, the same functionality can currently be emulated by directly referencing components of a vector, e.g.,

vec3 a = ( b[0], b[1], 1.0 )

A matrix is constructed by specifying a list of vectors. Each vector corresponds to a row (not a column) of the matrix. For instance,

mat2x2 A = ((1,2),(3,4))

initializes a 2x2 matrix where the top row has entries 1, 2 and the bottom row has entries 3, 4. Rows can also reference existing vectors, e.g.,

vec2 a1 = (1, 2)
vec2 a2 = (3, 4)
mat2x2 A = (a1,a2)

builds the same matrix as above. As with vectors, matrix entries can be undetermined. E.g.,

scalar d = ?
mat3x3 D = ((d,0,0),(0,d,0),(0,0,d))

describes a 3x3 diagonal matrix, where all three diagonal entries take the same, undetermined value d.

Vector and matrix element access

Individual elements of a vecN can be accessed using square brackets, and an index i between 0 and N-1 (inclusive). For instance,

vec3 u = (1,2,3)
scalar y = u[1]

will extract the y-coordinate of u (i.e., y=2). Matrix entries can likewise be accessed using double square brackets, e.g.,

mat2x2 M = ((?,?),(?,?))
scalar trM = M[0][0] + M[1][1]

constructs an expression for the trace of M. In this case, since M is not given explicitly in the Style program, the value of the trace will depend on the entry values of the optimized matrix.

As with any other variables in Style, assignment of vector or matrix entries is done in a functional manner (i.e. immutably in the computational graph).

Vector and matrix operators

The Style language supports the operations listed below on scalars, vectors, and matrices—here we will assume that c has type scalar, u and v have type vectorN, and A and B have type matrixNxN (all for the same N).

Note that elementwise multiplication .* and division ./ get applied independently to each entry of a vector or matrix. For instance, if u = (6,8,9) and v = (3,2,3), then the elementwise division operation u ./ v yields the vector (2,4,3) (i.e., six divided by three, eight divided by two, and nine divided by three).

Scalar-Vector

  • c * v — scales v by c (from the left)
  • v * c — scales v by c (from the right)
  • v / c — divides v by c

Scalar-Matrix

  • c * A — scales A by c (from the left)
  • A * c — scales A by c (from the right)
  • A / c — divides A by c

Vector-Vector

  • u + v — sum of u and v
  • u - v — difference of u and v
  • u .* v — elementwise product of u and v
  • u ./ v — elementwise quotient of u and v

Vector-Matrix

  • A*u — matrix-vector product Au
  • u*A — matrix vector product uᵀA

Matrix-Matrix

  • A * B — matrix-matrix product AB
  • A + B — sum of A and B
  • A - B — difference of A and B
  • A .* B — elementwise product of A and B
  • A ./ B — elementwise quotient of A and B
  • A' — matrix transpose Aᵀ

Vector and matrix functions

A variety of standard vector and matrix functions are also available in Style. Note that this list may not be up to date; see the Style Functions documentation documentation for an automatically-generated list of all available functions.

  • dot(u,v) — dot product of two n-vectors
  • norm(u) — norm of an n-vector
  • normsq(u) — squared norm of an n-vector, equal to dot(u,u)
  • unit(u) — unit vector parallel to u
  • rot90(u) — counter-clockwise 90-degree rotation of a 2D vector
  • rotateBy(u,θ) — rotates the 2D vector u by θ radians in the counter-clockwise direction
  • cross2D(u,v) — cross product of 2D vectors, giving the scalar u₁v₂ − u₂v₁
  • cross(u,v) — cross product of 3D vectors
  • outer(u,v) — outer product of two n-vectors, giving the matrix uvᵀ
  • angleBetween(u,v) — in 2D, unsigned angle between vectors u and v, in radians
  • angleFrom(u,v) — in 2D, signed angle from u to v, in radians
  • angleOf(u) — in 2D, angle made by u with the positive x-axis

Properties

2D vectors are also be used natively as the representation for shapes' coordinates (start, end, and center), with the appropriate getters and setters (e.g. A.shape.end[0]). Example:

forall VectorSpace U {               -- LinearAlgebra.sty
   scalar axisSize = 1.0 -- this is a local var
   vec2 U.origin = (?, ?) -- this is a globally-accessible var
   vec2 o = U.origin
   shape U.xAxis = Arrow { -- draw an arrow along the x-axis 
          start : (o.x - axisSize, o.y)
            end : (o.x + axisSize, o.y)
    }
}

Optional type annotations

Style supports optional type annotations on local variables and paths, e.g.

scalar v = 1.2

or

shape U.dot = Circle { ... }.

These type annotations are not typechecked, they're for readability only. (We hope to do real typechecking in the future.)

The list of supported Style types is:

  • scalar
  • int
  • bool
  • string
  • path
  • color
  • file
  • style
  • shape
  • vec2, vec3, vec4
  • mat2x2, mat3x3, mat4x4
  • function
  • objective
  • constraint

It's an error if you name a variable/path something that conflicts with a type name (e.g. U.shape is no longer allowed).

Specifying a custom initialization value for varying variables

If you want to declare a variable as varying and initialize it at a constant float value, you can do it with VARYING_INIT(i) (where i is the float value you want). Example use:

forall Vertex V {
       scalar V.val = VARYING_INIT(100.) —- Will be initialized at 100., then optimized

       shape V.icon = Rectangle {
         center: (V.val, VARYING_INIT(-200.)) —- Second coordinate will be initialized at -200., then optimized
       }

This can only be used by itself in either fields or vectors, not in expressions (e.g. VARYING_INIT(i) + VARYING_INIT(j) is not allowed).

Configurable Canvas Dimensions

In SIGGRAPH '20 the dimensions of the output SVG canvas were hardcoded to 800x700 pixels. The canvas width and height are now configurable by specifying numeric literal width and height values in the canvas namespace like so:

canvas {
  width = 800
  height = 700
}

Label selectors

It is now possible to check whether a Substance object has been assigned a label in a Style selector. This kind of selector can be useful for, e.g., conditionally drawing label text. For instance, you can write a Style rule of the form

forall Point p
where p has label {
   shape p.labelText = Text {
      string: p.label
      center: (?,?)
   }
   shape p.labelBox = Rectangle {
      center: p.labelText.center
   }
}

This rule will display the label and draw a rectangle around it, but only if p was assigned a label in the .sub program.

Math and text labels

Label selectors can be refined by checking whether a math or text label was specified in the Substance program. Such selectors can be useful for conditionally typesetting labels using either an Equation shape, which typesets TeX formulas via, MathJax, or a Text shape, which typesets text via an SVG text element. A math label is a TeX string delimited by dollar signs, e.g.,

Label p $p_0$

whereas a text label is a plain-text string delimited by double quotes, e.g.,

Label p "a point"

In Style, these two different label types can be matched using the selectors

where p has math label

and

where p has text label

respectively.

Canvas dimensions must be defined in Style or else the Penrose compiler will throw an error.

Other than these fields' effects on the output canvas's dimensions and their restriction to numeric literals, the canvas namespace acts like any other namespace: you can define the namespace anywhere in the program; you can override the fields; and you can add other fields to the namespace without any special restrictions.

More concretely, the following Style programs are valid:

...my cool Style code...

canvas {
  width = 200
  height = 30
}

...more of my awesome Style code...
canvas {
  width = 800
  height = 700
}

canvas {
  override height = 800
}
canvas {
  width = 800
  height = 700
  foo = ?
  bar = (1., 1.)
}

The following Style programs are not valid:


...my cool style program but canvas dimensions are not defined!...

Error: Canvas properties are not defined. Try adding:

canvas {
  width = <my desired width>
  height = <my desired height>
}

canvas { 
  width = 100
}

Error: Canvas height is not defined. Try adding: canvas { height = <my desired height> }


canvas {
  width = Circle {}
  height = 100
}

Error: Canvas width must be a numeric literal, but it is a shape.


canvas {
  width = ?
  height = 100
}

Error: Canvas width must be a numeric literal, but it is an expression or uninitialized.


canvas {
  width = (1.0, 1.0)
  height = 100
}

Error: Canvas width must be a numeric literal, but it has type Vector.

maxSize Now Requires an Explicit Maximum Size

In SIGGRAPH '20, maxSize implicitly used the minimum of the canvas's width and height to compute the maximum size of objects constrained by maxSize. Since the canvas update described above, the interface is now maxSize(variable, bound). The old functionality can be achieved by writing maxSize(variable, min(canvas.width, canvas.height)).

delete keyword

Elements of a Style block (including shapes, constraints, and penalties) can be deleted in a later, cascading block. This feature can be helpful for, e.g., removing visual elements for a subtype. For instance,

-- by default, draw a circle for all instances of type T
forall T x {
   x.widget = Circle { ... }
}

-- but don't draw this circle for instances of a subtype S <: T
forall S x {
   delete x.widget
}

Note that one must be careful not to reference deleted attributes in a later generic block. For instance, the following block will produce an error if invoked for an instance of S:

forall T x {
   shape x.newWidget = Circle {
      center : x.widget.center -- not defined for instances of S
   }
}
Clone this wiki locally