Substroke Design Dump: Language Details

Bret Victor / April 17, 2007

Panels

A panel is equivalent to a "function application" in a conventional language. This is how it appears:

This is how it looks with parameters:

The function name here is "enclose". The function name can be any name in the current scope.

The parameter name here is "with". Its strip here has a single panel, but it can be of any length and have properties of its own.

Parameters override properties in the function object. A parameter name refers a function's property name. A parameter value is a strip. The parameter is lazy -- the strip is not evaluated until it is "used".

The example displays the result of the function application, also called the "value of the panel". This is a single object.

The description names the object in the example. This is, by default, the name of the object constructor that created the object. When the description says "4 objects", that is shorthand for "Group of 4 objects".

Strips

A strip consists of one or more panels. It is similar to a "closure" in a conventional language.

A strip evaluates to its final panel. The final panel is graphically emphasized to call attention to it.

Each panel is passed the value of the previous panel (the panel to the left) as an implicit parameter, called the direct object. This too is lazy -- if a function doesn't take a direct object, its previous panel is not evaluated.

The first panel in the strip is passed the direct object of the enclosing function.

If a function has no direct object, then we look at its lexically-enclosing function, and so on.

Names

Names follow filename rules: They are case-insensitive. Spaces are allowed. Most punctuation is allowed.

By convention, names of "verbs" -- functions that take a direct object -- are shown in lowercase. Names of "nouns" are shown in uppercase. The casing is performed automatically by the IDE.

There are only two ways to define a name -- object properties and panel labels:

There is only one way to dereference a name -- function application:

Names are mostly lexically scoped. (The exception is with (extends), although that is sort of "conceptually lexical".)

Labels

Panels can be labeled.

The scope of this name: The name can be used in a function application anywhere in the strip (including previous panels) and anywhere in any property of the strip.

Applying the name with no parameters results in the value of the named panel.

Applying the name with parameters: The labeled panel is evaluated. The properties of the resulting object are overridden with the parameters. This object with overridden properties is returned.

The direct object can be labeled, if panels other than the first need to refer to it.

It

The name "It" behaves like a label. When used within a function application, it refers to the previous panel. If there is no previous panel, it refers to the direct object of the enclosing function. If the enclosing function has no direct object, then we look at its enclosing function, and so on.

Layers

A panel can contain one or more "scratch layers". Each layer contains one object.

Layers are numbered. Layer 1 is the base layer. All parameters are passed and results returned on layer 1.

Layers appear superimposed in the panel. Layers other than 1 are tinted a particular color.

Layers that are not changed in a particular panel are inherited from the previous panel.

A function application can specify which layer the result should go to. (This is how layers are created.) If no target layer is specified, the default is layer 1.

In a function application, the function name can be colored. The color specifies which layer of the previous panel is passed as direct object to the function.

If the function name is a label, color also specifies which layer of the labeled panel is being referenced. (These two uses are mostly exclusive, since labels don't take a direct object.)

In the IDE, layers can be Z-reordered by dragging the description up or down. A layer can be removed by deleting its description.

A layer can be created "linked" to another layer. This means that changing one layer causes the other layer to change as well.

Property Linking

A layer can be linked to another layer's property. This linking is a syntax tranform, performed at compile time.

When the source layer is changed, the target layer updates to get the property. The strip above is translated by the compiler to:

When the target layer is changed, the source layer's property is set.

The strip above is translated by the compiler to:

Generic Linking

A layer can be linked to another using a linking function.

When the source layer is changed, the function is called to update the target layer. When the target layer is changed, the function is called to update the source layer.

Again, the link is a syntactic transform, not part of the core language. Here is how the "macro" expands:

Here is a possible implementation of "smileness":

Note that "Link From" is (required) in the first definition. The first definition implements the target-to-source link, and the second definition implements source-to-target. Note also that "Link Source" (the value of the source when the link was created) is ignored here, although it is needed for other linking functions, such as "select" below.

The "select" function below links to the subset of a Group that is within a region. This subset can be manipulated by manipulating the linked layer. Even if the selected members are moved outside the region, they are still selected.

Indices

"Its #1" is an alias for "Its First". "Its #2" is "Its Rest's First", and so on.

"Its #1,etc" is the same as "It". "Its #2,etc" is the same as "Its Rest". "Its #3,etc" is "Its Rest's Rest".

"Its #2,3,4" returns a Group with the three specified members. "Its #2,5,etc" returns a Group with #2, #5, and everything that follows #5 (which could be infinite).

A layer can be linked to these indices. This behaves like a property linking. Changing the target layer results in a group on the source layer with the selected member replaced accordingly.

The selection is performed in the IDE by clicking on objects in the example picture, not by typing in "#2,3".

I'm considering simplifying the syntax to "Its 2", "Its 2,5,etc". (The # is unneeded.)

Nothing

Nothing is the "empty picture", equivalent to "nil" or "null" or the "empty list" in other languages. It is an object that has no properties, and a blank appearance. It is also returned (rather than bottom) when all definitions of a function fail their requirements.

Properties

A function can have properties.

Properties have a name and a strip.

The scope of a property name is the entire function, including the function's strip and other properties.

When a function is applied, the applied parameters override the corresponding properties.

Because a property is a function, properties themselves can have properties:

Properties are lazy -- they are not evaluated until "used".

Properties are the only means of abstraction. They serve the purposes of subroutines, methods, data structure members, etc in other languages.

All properties are public (for now).

If an object doesn't have a particular property, trying to get that property results in Nothing.

#1 is an alias for "First". #2 is an alias for "Rest's First", #3 is "Rest's Rest's First", etc.

Constructors

If a function has no strip, then it returns an object. The function is called a "constructor".

The object has the function's properties, including parameters passed during application.

Functions can pass parameter names that aren't property names. In this case, the properties are added, although (maybe) they cannot be seen from within the object's scope.

Groups

A group is called a linked list in other languages. It is an object with a "First" parameter and a "Rest" parameter.

The objects in the group appear (by default) from top to bottom.

A group may be infinite if successive evaluations of the "Rest" property do not terminate. This is okay. (The IDE displays infinite pictures with a dotted panel border.)

Appearance

All objects have a property named "Appearance". An object is rendered by recursively evaluating Appearance properties until primitives are reached. ("Appearance" is only special to the renderer, not to the language.)

Defaults

The values of properties in a definition serve two purposes. They are defaults, meaning that these values are used if they are not overridden by a parameter in function application. They are also examples, meaning that they are what the artist sees and works with when drawing.

Sometimes, the artist may want the default and the example to be different. The (example) attribute is used for this. Values defined with the (example) attribute do not exist at runtime.

In a definition, the direct object is implicitly an (example), since it is always overridden.

Getting properties

An object's property can be extracted with the possessive syntax:

"its" and "it's" are synonymous.

Possessives may be chained:

Setting properties

Objects are immutable, so "setting" a property refers to cloning the object, with that property changed.

There are two ways to do this: function application and (extends). They are conceptually different, in that they are used for different purposes. However, they behave similarly -- their main technical difference has to do with scoping.

Function application

The parameters passed when applying a constructor or a named panel appear in the object.

If Foo is a constructor or label, this should normally be the identity function:

In function applications, parameter strips capture (close over) their lexical scope. Even if the parameter becomes a property of an object, the names in that property's strip are looked up in the captured scape. The parameter strips cannot access the new object itself; that is, there is no "self" or "this". (That's what (extended) is for.)

(extended)

This should probably be named something else.

One or more properties may have the (extended) attribute.

When the object is constructed, the extended property is evaluated, and all of its properties are copied to the new object. (An object may thus be somewhat strict in these properties, although it is possible to delay their evaluation until a property lookup fails.)

The copied properties appear as if they were defined underneath the given properties.

For multiple (extended)s, the properties appear as if they were copied in the order given.

Importantly, the inherited and new properties are all in the same scope. However, there is a subtlety. A new property can refer to any property in the object, new or inherited. But inherited properties can only see new properties if they override an inherited property with the same name, and from the same base object. If a base object refers to a name outside of itself, that external binding will always hold, even if the child object defines a property with the same name. This avoids a name-capture situation that would always be a mistake.

If a (required) property fails, it will fall through to inherited properties, as if they had actually been defined in that scope. This allows for a conditional override.

Extending is dynamic. An (extended) property can refer to the direct object and other properties, including passed parameters.

Dynamic extension allows for mix-ins.

This is possibly the only aspect of the language where time/ordering is significant: The object extends each extended property in the order given. When the extended property is being evaluated, no properties have been inherited from it (of course) or from later extended properties. This means that a property may have a different value when it is evaluated during an (extended) than it does when the object is fully constructed. In practice, extended properties should be very simple and not depend on things that will change during extending.

Sometimes, it may not be necessary to refer to the (extended) property itself by name. In this case, it can be anonymous:

If it is given a name, the name is used slightly differently than a normal property. Getting a property from an (extended) property is like "super" in other languages; it evalutes the property in the context of the child object. This allows the artist to get to properties that have been overridden. This is especially useful for overriding the Appearance property.

(required)

A function may have multiple definitions.

Some of the function's properties may have the (required) attribute.

When the function is applied, the first definition's (required) properties are evaluated. If any of them evaluate "false", the second definition is used, and so on. If all definitions fail, the function evaluates to Nothing.

"False" usually means Nothing. However, in certain situations (maybe) you can end up with a Nothing that has had properties applied via function application. Such an object is still false for the purposes of (required).

The direct object is implicitly (required). This means that, for a typical function, passing it Nothing results in Nothing. This eliminates an explicit base case in many recursive functions.

To make the function to return something when passed Nothing, specify a second definition of the function that doesn't take a direct object.

A function with a strip is strict in its (required) properties. An object constructor is somewhat strict, although it is possible to delay their evaluation until a property lookup fails. (That is, some objects can be usefully used without forcing their (required) properties.)

(code)

A property may have the (code) attribute.

The value of the property is a block of text in the host language. (Lua, in this case).

A property may have a (code) definition and a strip definition. The (code) definition is used at runtime, but it is possible to run tests to verify that the two definitions are equivalent. This allows (code) to be provided for optimization, while keeping the Substroke definition for documentation.

(code) is also used for "magic" properties, such as the Appearance properties of primitives and Groups.

There is a well-defined API that allows code to manipulate objects. Code runs in a sandboxed environment that enforces purity (ie, no state can be saved between calls). However, via the "unsafe" API, code can do various unsafe things (network access, file access, store mutable state, store persistent state) subject to a fine-grained privilege model.