Stemscript

Stemscript is the scripting language used for creating tools like [modules], [blocks] and macros inside Blockpad.

It is loosely based on Javascript and spreadsheet formula language. In fact, the math formulas used in Blockpad equations are a subset of Stemscript, so the syntax used for Blockpad formulas (including things like units) also works when using Stemscript for scripts.

Design Approach

Since Stemscript was built to work in formulas, it is very similar to conventional spreadsheet formulas, with a few notable differences.

  • First-class intuitive built in units
  • JSON style arrays and objects
  • C and Javascript style control flow (for things like for loops)
  • JSX style syntax for specifying content in script block definitions

Good to know

Some useful things to know about using Stemscript for scripts, especially if you're mostly used to formulas.

  • All new variables need to be declared by putting the keyword var in front of them.
    • e.g. var B = 10 ft; or var Length;
  • Use a semicolon to mark the end of a coding line. Actual line spacing does not impact the code.
    • Semi-colons do not follow curly brackets {}, for loops, or if/then statements.
  • Variables inside of a script can be overwritten.
  • Array items in a script can be overwritten using bracket indexers.
  • The values in key-value objects can be overwritten using dot notation.
  • In a script, Blockpad libraries need to be explicitly included or referenced.
    • For example, if you want to use the Sqrt() function, you either need to include the math library at the top or write Library.Math.Sqrt().
    • To include a library (for example the math library), type in the line include(Library.Math); at the top of the script.

Basic Value Types

Numbers

Numbers are double-precision floating point combined with a unit. Numbers without a unit have null as a unit.

Example number literals
25
0.25
25 ft
0.25 gal
1/4
3 3/4
3 ft 6 1/2 in 
6 lbf/in^2

Strings

A string is a piece of text consisting of Unicode characters. Single or double quotes can be used to specify a string. Use backslash to escape characters.

Example string literals
"example"
'strings'
"He said, \"Good morning\""

Boolean

A boolean value is a true or false logic value.

Boolean literals
true
false

Arrays

Blockpad supports 2-dimensional arrays that can hold any value type as an item. 3+ dimensional arrays can be achieved by nesting arrays inside of arrays. Matrix math is supported with number-only arrays.

Brackets specify an array. Commas separate a row into columns and semi-colons specify the end of a row.

Example array literals
[1; 2; 3]

["hello", "how are you", "I'm fine", 12 in^3, true, 11111]

[1 in, 2 ft, 3 m; 4 lbf, 5 kip, 6 kN]

[[1,2,3], [4,5,6]; [7,8,9], [10, 11, 12]]

Functions

A function evaluates given inputs based on a fixed definition and returns an output. There are many built-in functions, but new functions can be defined.

Example function literals (inline functions)
x => x^2

(x, y) => Sqrt(x^2 + y^2)

(height, width, length) => height * width * length

(cylRadius, cylHeight) => {
    var bottomArea = pi*cylRadius^2;
    var volume = bottomArea*cylHeight;
    return volume;
}
Special syntax for named functions

There are a few ways to define a named function, in addition to just using a function literal. The methods that use curly braces can be multiple lines long.

f1 = (x, y) => x^3 + y + 1

f1(x, y) = x^3 + y + 1

f1 = function(x, y) { return x^3 + y + 1; }

f1(x, y) { return x^3 + y + 1; }

function f1(x, y) { return x^3 + y + 1; }

f1(x, y) = program {x^3 + y + 1;}

Key-value objects

Key-value objects hold a list of key-value pairs, where each "key" works like a name for a corresponding value. They are a lot like Python dictionaries or Javascript objects.

Keys are strings, but it isn't necessary to specify them as such in an object literal. Values are any other value type (number, string, array, object, etc).

Example object literals
{length: 0.7 ft, width: 20 mm, material: "mithril"} 

{name: 'Bilbo Baggins', species: 'Hobbit', height: 3 ft}

Variables

Variables need to be explicitly created using the var keyword. E.g. var VariableName. Create multiple variables at once by separating them with commas. E.g. var Name1, Name2, Name3;

You can assign a value in the line where they are created, or you can create them without assignment, and assign a value later. The value assigned can be a value literal or the result of expressions.

Once created, you can overwrite the value (or lack therof) assigned to a variable. No need to rewrite var each time it's used.

Examples
var length, width, area;
length = 4 ft;
width = 1.2 m;
area = length*width;
var a = 5 in*pi;
var b = a*10 in;

Operations

Math operations

Math operators are based on spreadsheet formulas, so generally what works in a spreadsheet works here. Math operations are units sensitive. For example, 3 m + 5 ft = 4.524 m, not 8.

+ Addition
- Negation and subtraction
* Multiplication
/ Division
^ Exponents

To assign units to a number literal, type the unit after the number, with or without a space. Compound units can be created using *, /, and ^.

var force = 5 lbf;
var area = 11m^2;
var cost = 99 usd/year;
var torque = 12.1kip*ft;
var pressure = 0.01 lbf/cm^2;

To convert a number to different units, use the to keyword and the new units after. In the example below, note that the first two conversions don't have an affect on the result for area. They would only matter if you want to view width or length.

var length = 5 m;
length = length to in;      //convert length to inches

var width = 7 m to in;      //enter width as meters and convert to inches

var area = length*width to ft^2;    //calculate area and convert to square feet

See the [units page] for comprehensive coverage of handling units.

Boolean operations

Boolean tests are listed below. Testing equality (==) is one of the few departures from typical spreadsheet formula language, but this departure is consistent with the formula language inside Blockpad.

== Equal to
!= Not equal to
> Greater than
>= Greater than/Equal to
< Less than
<= Less than/Equal to

Library Functions

Blockpad has a number of built-in functions, including most functions found in conventional spreadsheets (e.g. Max(), Sqrt(), Vlookup()) These built-in functions are stored in various libraries.

To use built-in functions in a script, the library either needs to be included, or referenced as part of calling the function.

To include a library in a script, use the following syntax at the top:

include(Library.Math);

Below is a list of common libraries to use in a script, you can find some of them library page. Also, the [scripts library] included important functions like Each() and For().

include(Library.Math);
include(Library.Scripts);
include(Library.Text);
include(Library.Logic);
include(Library.DateTime);
include(Library.Docs);
include(Library.Geometry);
include(Library.Lookup);

To call these functions use open and close parentheses, with the arguments separated by values inside, like in a spreadsheet.

Calling a built-in function example
var maxVal = Max(1, 4, 7);
var avg_1 = Average(1, 4, 7, 10);

If the library is not included, you can still use the function by writing out the full library reference.

var sqrtVal = Library.Math.Sqrt(100);
var first4Chars = Library.Text.Left("abcdefg");

String operations

Most text operations are done through functions from the text library. See the [deep dive functions page] for more on what's available.

Concatenate strings using the + operator or the Concat() function.

var word1 = 'Good';
var word2 = 'morning';

var sentence1 = word1 + ' ' + word2 + '!'                       // = 'Good morning!' 

Array operations

Other resources:

  • [The array section of the deep dive]
  • [Functions for working with arrays]

Math operators

There are two ways to interpret math operators with arrays: matrix math and item-by-item. Blockpad takes a matrix math first approach.

So, if it's possible to interpret a math operator as performing matrix math, then that is what Blockpad will do. For addition, subtraction, and multiplying by a scalar value (i.e. non-array value), matrix math and item-by-item are the same.

For multiplication, division, and exponentiation, Blockpad will perform matrix math if possible. E.g. [1, 2;3, 4] * [ 3, -1; -2, 2] = [-1, 3; 1, 5]

If matrix math is not possible, and the arrays are one dimensional in the same direction (i.e. both have one row or both have one column), then Blockpad will perform item-by-item calculations. E.g. [1; 2; 3] * [2; 2; 1] = [2; 4; 3]

Array indexing

Arrays can be indexed with either brackets or parentheses. Brackets are indexed to 0, while parentheses are indexed to 1. You can use these indexers either to reference a value in an array or to overwrite that value.

var array_1 = [1, 2, 3];
var item_2 = array_1[1];    // = 2 (item_2 is a copy of the indexed value)
array_1[2] = 45;            // now array_1 = [1, 2, 45]

var array_2 = [4, 5, 6];
var item_3 = array_2(2);    // = 5 (item_3 is a copy of the index value)
array_2(3) = 55;            // now array_2 = [4, 5, 55]

Using indexers to overwrite values won't work on some types of arrays, including arrays from table ranges, the results of some functions like Each() or LinearSeries(), and the results from item-by-item operations. If you need to use indexers on these arrays, use the Copy() function on them to create a copy that will work with indexers.

var array_3 = Each([1, 2, 3], x => x*2);    // = [2, 4, 6]
array_3[0] = 10;            // this will give an error
array_3(1) = 10;            // this will give an error

var array_4 = Copy(Each([1, 2, 3], x => x*2));    // = [2, 4, 6]
array_4[0] = 10;            // now array_4 = [10, 4, 6]
array_4(2) = 20;            // now array_4 = [10, 20, 6]

Bracket and parentheses indexing also works for 2-dimensional arrays, with the form being (row, column). For a 2-dimensional array, just providing one index number returns that row as an array. You can use * to mean all rows or columns, so (*, columnIndex) will return that column as an array.

var array_2dim = [  1, 2, 3; 
                    4, 5, 6   ];

array_2dim[0,1]     // = 2
array_2dim[1]       // = [4, 5, 6]
array_2dim[*, 2]    // = [3; 6]

array_2dim(1,3)     // = 3
array_2dim(*, 3)    // = [3; 6]

If the index of the value you are overwriting isn't contained in the array, then the size of the array will be increased to fit it. The missing index values will be a null value.

var array_2dim2 = [  1, 2; 
                    4, 5   ];

array_2dim2[2,2] = 10;
    // = [1, 2, null; 2, 5, null; null, null, 10]

Initiating an array

To initiate an empty array, use open and close brackets []. For an array of only zeros, use the Zeros() function, and for an array of null values use MakeArray(). Arrays initiated in this way will work with array index overwrites.

A = [];
B = Zeros(3, 5);
C = MakeArray(3, 3);

Key-value object operations

Object values are accessed with a period (.), followed by the key. New key/value pairs can be created using this, and values can be overwritten.

var person1 = { name:    'Bilbo Baggins', 
                species: 'Hobbit', 
                height:  3 ft        };

person1.name;             // = 'Bilbo Baggins'
person1.age = 111 year;   // the age key is added to person1, with value of 111 year
person1.age = 112 year;   // age is changed to 112 year

See the [deep dive functions page] for functions that work with key-value objects.

Control flow

if statements

Blockpad has if/else/else statements similar to C-style programming languages like javascript. Note that there is no semicolon after the curly brackets.

if (condition) {
    //code to be run if condition is true
}
//continues after, whether or not code in the block is run
if (condition) {
    //code to be run if condition is true
} else {
    //code to be run if condition is not true
}
//continues after
if (condition1) {
    //code to be run if condition1 is true
} else if (condition2) {
    //code to be run if condition1 is not true, but condition1 is
} else {
    //code to be run if neither condition1 or condition2 is true
}
//continues after

Examples

var force = 100 lbf;
var check;

if (force > 200 lbf) {
    check = "force is too large";
} else if (force < 50 lbf) {
    check = "force is too small";
} else {
    check = "force is ok";
}


var shape = {type: 'triangle', dim1: 1in, dim2: 3in};

if(shape.type == 'triangle') {
    shape.area = 0.5 * shape.dim1 * shape.dim2;
} else if (shape.type == 'square') {
    shape.area = shape.dim1 * shape.dim2;
} else {
    shape.area = 'undetermined';
}

The If() function

You can also use, the If() or Ifs() functions, which work like conventional spreadsheet functions.

If(condition, value if true, value if false);

This can be just as powerful as if blocks by using function calls as the return values.

result = If(condition,
            functionToRunIfTrue(),
                functionToRunIfFalse()
);

For loops

Stemscript supports two styles of for loops.

  • A conditional C-style three-part for loop.
    • for (initial value, condition, step at end) {what to do in the loop}
    • Simple example: for (step = 1; step <=3; step++) {A(step) = step^2;}
  • A determinate for-in loop that loops through items in an array.
    • for (var name in array) {what to do in the loop}
    • Simple example: for (var step in stepsArray) {A(step) = step^2;}

In this example, both styles of for loops are used to get the sum of an array.

var diameters = [1in; 1.5in; 2in; 3in; 3.5in];

var sum1 = 0;
var i;
for (i = 0; i < CountAll(diameters); i++) {
    sum1 = sum1 + diameters[i];
}

var sum2 = 0;
for (var diam in diameters) {
    sum2 = sum2 + diam;
}

There is also a For() function, which works as a determinate for loop that runs a function over a given array. Here is the same example as above using the For() function.

var diameters = [1in; 1.5in; 2in; 3in; 3.5in];
var sum3 = 0;
For(diameters, diam => {sum3 = sum3 + diam;});

Library function alternatives to loops

Blockpad has library functions that can work as alternatives to for loops. These boil down to [mapping functions like Each(), EachRow() and EachColumn()] and the general reducing function [Reduce()]. In many cases, using [item-by-item] operations or [built in functions] like Sum() or Filter() also work as alternatives to for loops.

Use [Each()] to perform a function on each item in an array:

var diameters = [1in; 1.5in; 2in; 3in; 3.5in];
var areas = Each(diams, d => (pi/4)*d^2);

Use Reduce() to combine the items in an array to a final result, e.g. to take the sum of an array.

var diameters = [1in; 1.5in; 2in; 3in; 3.5in];
var sum4 = Reduce(diameters, 0, (sum, diam) => sum + diam);

While loops

Stemscript supports C-style while loops and do/while loops.

  • while (condition) {/*code loops until condition is met*/}
  • do {/*code runs once, then loops until condition is met*/} while (condition)

Note that there is no semicolon at the end of the curly braces for the loop, and there is also no semicolon after while(condition) for a do/while loop.

Examples

//increments until value meets requirement
var length1 = 0in;
while (length < 5.31 ft) {
    length = length + 1 in;
}

//same as above, but does at least one increment first
var length2 = 0in;
do {
    length = length + 1 in;
} while (length < 5.31 ft)

//A custom function that returns the cube root of a value (uses math library functions)
CubeRoot = (val) => {
    target = Magnitude(val);
    x = target / 2;
    tol = 1e-9;
    delta = 1;
    while (Abs(delta) > tol) {
        delta = (x^3 - target) / (3*x^2);
        x = x - delta;
    }
    x*(Unit(val)^(1/3));
}

//A custom function that calculates the value that has a factorial under the given value
MaxFactorialUnder = (limit) => {
    n = 0;
    fact = 1;
    do {
        n = n + 1;
        fact = fact * n;
    } while (fact * (n+1) <= limit)
    n;
}

try/catch

Try/catch statements will try to run a piece of "try" code, but if there is an error in the try code, the "catch" code will run instead. The syntax is similar to if/then statements, but using the try and catch keywords.

Errors that will be caught include division by zero, unit errors, or mismatched value types (e.g. trying to take the sum of text values).

try {/*code to try first*/}
catch {/*code to run if there is an error in the first code*/}
//Example:
//If it's possible that area is 0 in the calculations, and you want to keep running the code, you can use a try/catch block

var force = 10 kip;
var area = 0 in^2;
var stress;

try { 
    stress=force/area; 
} catch { 
    stress = 0 psi; 
} 

In addition to try/catch statements, you can use the Try() function for simpler equations.

var force = 10 kip;
var area = 0 in^2;
var stress = Try(force/area, 0 psi);

break

The break keyword is an early exit from a loop. After a break, the code will continue running after the loop entirely - the remaining iterations will not be run.

//To break out of a loop, just have the keyword break as a line of code:

break;

 
//Example:
//Sum numbers 1 to 6, but stop when you get to 4 (so really just sum numbers 1 to 3)

var sum = 0; 
for (i = 1; i <= 6; i++) { 
    if (i == 4) { 
        break; 
    } 
    sum = sum + i; 
}
var result = sum;   //=1 + 2 + 3 = 6
 

continue

The continue keyword is an early exit from a loop iteration. After continue, the code will continue running in the next iteration of the loop, without completing any of the remaining code in the original iteration.

//To continue to the next loop iteration, just have the keyword continue as a line of code:

continue;

 
//Example:
//Sum numbers 1 to 6, but skip summing the number 4

var sum = 0; 
for (i = 1; i <= 6; i++) { 
    if (i == 4) { 
        continue; 
    } 
    sum = sum + i; 
}
var result = sum;   //=1 + 2 + 3 + 5 + 6 = 17
 

return

The return keyword is an early exit from a function block (when you use curly brackets to define a function). The return keyword specifies what the result should be for a function and exits running the code inside the function.

//to use the return keyword, use the word return and proved the result after in the line of code;

return varToBeReturned;
//Example:
//Return various messages depending on the inputs

SlendernessClass = (KL, r) => {
    if (r <= 0 in) {
        return "invalid: r must be positive";
    }
    if (KL <= 0 in) {
        return "invalid: KL must be positive";
    }

    var ratio = KL / r;

    if (ratio < 50) {
        return "short";
    }
    if (ratio < 120) {
        return "intermediate";
    }
    return "long";
}

var message = SlendernessClass(10ft, 1ft);  //  = "short"