Stof is designed to be very easy to use. It is not a general-purpose language; it only exists as a scalable, secure, and portable interface to data and remote systems (APIs).
In general, Stof should be considered a document rather than a program. One of its main goals is to be a medium for different data types to come together (data unification). The language is only one of these data types, acting as a controlled interface to the others.
If the value were an object, the field could be written as:
field: {
// additional fields in this object
}
// OR
obj field: {},
// OR
obj field: {};
Value Expressions
Fields get parsed from the top of the document downwards, executing each value expression as they are seen.
fnhello():str { return"hello"; }str field: self.hello() // creates a field with the value "hello" ("str" optional)
If we reversed the order of the function declaration and field declaration in this example, the document would fail to parse, resulting in an error.
However, if a function is parsed that references a field that has not yet been parsed, this is fine as long as it isn't called until after the document is parsed (or all fields/functions it references).
This functionality can help improve the readability of your documents as well:
cm height: 6ft + 1in; // casts 6ft + 1in to centemeters when parsed
Block Expressions
Stof also has block expressions, which can be used when defining fields (or within functions of course).
field: { // This isn't an object - Stof will execute while parsinglet val =12+100;return val;};#[test]fntest() {assertEq(self.field, 112);}
Objects
Objects are defined as fields, but start a new scope that can contain additional fields, functions, and types (along with other data if you choose).
See Objects for more information on objects in Stof.
obj myobject: { // remember, "obj" type here is optionalstr field:"hello, there"}
Arrays
Array fields are defined in a familiar way.
vec array: [ // just like JSON, and the "vec" type is optional {// can have fields, functions, types, etc... no different than outer objects } as obj, // optional syntax for casting objects to complex types from an arraytrue,"hi",32]
Functions
See Functions for more information on functions in Stof. This is merely a syntax reference.
Functions in Stof should feel pretty familiar, following the general syntax:
#[attribute]fnhasAttribute() {}fnnoAttributes() {}#[a] // attribute with a value of null#[b("hello")] // attribute with a value of "hello"fnmultipleAttributes() {}
Statements
Statements are used in functions and code blocks to manipulate the document, perform transformations, and execute custom logic.
Variable Declaration
A variable is declared in Stof using the "let" keyword. Variables are not fields, but field values can be loaded into variables for mutation.
fnexample() {let a =32; // declaration with implicit typelet b: int =43; // declaration with explicit type (casts if needed)}
Assignment
Assignment is special in Stof as variables can be assigned to, sure, but fields in the document can be set this way also.
field:30fnexample() {let val = self.field; val = val +10; // val is 40, field is still 30 - could also use "+=" here... self.field = val; // sets field to 40}
Fields are not references, as seen in this example. This makes Stof much easier to use in real life, where undoing field manipulations on errors, etc. can be quite difficult or impossible.
It also improves the readability and maintainability of Stof, knowing exactly when and where fields are being set vs variables.
In some scenarios (like pushing to an array that is a field), this can be annoying. However, it's worth the minor inconvenience of another assignment.
Addition Assignment
Assignments to fields are allowed in all explicit assignment statements.
fnexample() {let val =10; val -=5;assertEq(val, 5);}
Multiplication Assignment
fnexample() {let val =2; val *=2;assertEq(val, 4);}
Division Assignment
fnexample() {let val =10; val /=2;assertEq(val, 5);}
Modulo Assignment
fnexample() {let val =3; val %=4;assertEq(val, 3);}
Drop
The "drop" statement does exactly what it sounds like, frees variables. It can also delete fields, functions, and objects from the document entirely.
field:"hi"#[test]fnexample() {let val =20; drop val;assertNull(val); drop self.field;assertNull(self.field);}
If Statement
An "if" block is required, followed by optional "else if" blocks, and finally, an optional "else" block.
fnexample() {let expr =true;let elif =false;if (expr) {// execute these statements if expr evaluates to a truthy value } elseif (elif) { // can have multiple "else if" blocks// execute these statements if expr is falsy and elif is truthy } else {// execute these statements if expr is falsy and elif is falsy }if (true) pln("don't need braces either for a single statement...");}
Switch Statement
Switch statements evaluate an expression, matching the resulting value to a specified "case", or the "default" case if one is defined and a matching "case" was not found.
Cases in a switch statement do not fall-through to the next case in Stof (like other languages) if a break or return is not found. Instead, Stof offers the "|" or operator to match multiple cases to a singular block of statements.
fn example() {let val ="a";switch (val) {case"b": pln("doing b things");case"c": {// things related to c }case"d": {},// optional comma to separatecase"e": {}; // optional semi-colon to separatecase"f"| case "g": {// handle multiple cases in one block... }default: {// optional default block to handle any value not matching a defined casepln("received: ", val); } }}
Try-Catch
Try-catch blocks are pretty simple in Stof, consisting of a singular try block and a singular catch block (no finally). Catch blocks are not required to, but can optionally catch the error type and message.
See Error Handling for more information on handling errors in Stof.
fn errors(i: int) {if (i <0) throw("you are wrong");}#[test]fn handles() {try {self.errors(-34); } catch {pln("I was wrong"); }}#[test]#[errors] // tests to make sure this test throws an errorfn doesnt() {self.errors(-10);}#[test]fn message() {tryself.errors(-34);catch (message:str) {assertEq(message,'you are wrong'); }}#[test]fn type() {trythrow('CustomError','dude, this is not the right way');catch (error: (str, str)) {assertEq(error[0],'CustomError');assertEq(error[1],'dude, this is not the right way'); }}#[test]#[errors]fn map() {trythrow('MyError','not implemented');catch (error:map) {assertEq(error.get('message'),'not implemented');assertEq(error.get('type'),'MyError');// function stack to where the error occurred// last function in the stack is the one that threw an errorassert(error.get('stack').len() >0);// continues the error up the stack with an additionthrow(error.get('type'),'map error: '+error.get('message')); }}
Return
fnemptyReturn(): void {return;pln("hello, there"); // never called...}fnreturns():str {return"hello"; // passes the value "hello" to the callerpln("dude"); // never called...}
While Loop
#[test([8, 6, 4, 2])] // asserts that the return value equals [8, 6, 4, 2]fnexample(): vec {let res = [];let val =10;while (val >0) { val -=1;if (val %2==0) { res.push(val);continue; // go directly to the next iteration of the looppln("never gets printed"); }if (val <3) break; // exits the loop }return res;}
Just like in other programming languages, both the "break" and "continue" statements only apply to the most immediate loop (including for-loops).
For Loop
fnexample() {let res = [];for (let i =0; i <5; i +=1) res.push(i);assertEq(res, [0, 1, 2, 3, 4]);}
In real life, there's a range expression for constructing arrays of this nature:
<start (inclusive)>..<end (exclusive)>|<step>?.
The above "res" array could be defined with the expression: 0..5.
For-in Loop
The for-in loop is the easiest and most common way to iterate over collections and objects in Stof. Any value that implements a "len" and "at" function can be iterated over using a for-in loop. This includes most of the value types in Stof by default: vectors, sets, maps, objects, tuples, blobs, numbers, and strings. Take a look at each library, the "at" function will tell you what type of value to expect.
Each for-in loop defines 3 variables (in a separate scope) that commonly come in handy while iterating:
first - true if inside the first iteration of the loop, false otherwise
last - true if inside the last iteration of the loop, false otherwise
index - the index of the current value
#[test([10, 1, 2, 3, 4, 30])]fnexample(): vec {let array =10..34|4;let res = [];for (i in array) {if (first) res.push(i); // first = true when i is the first value in the arrayelseif (last) res.push(i); // last = true when i is the last value in the arrayelse res.push(index); // i = array[index] }return res;}
Custom Object Iteration
myobj: {fnat(index: int): unknown {return index *12; // default is to return the field value }fnlen(): int {return5; // for objects, the default is the number of fields }}#[test([-100, 13, 26, 39, 100])]fncustomIteration(): vec {let res = [];for (val in self.myobj) { // default val would be a tuple (key, value) of a field on myobjif (first) res.push(-100);elseif (last) res.push(100);else res.push(val + index); // 12 + 1, 24 + 2, 36 + 3 }return res;}
Expressions
In this section, we'll highlight some of the most handy and used expressions Stof has - this is not a complete list of expressions available.
Function Calls
fnhello():str { return"hello"; }#[test]fnexample() {let val = self.hello(); // call is an expression, part of a declaration statement}
Tuple Construction
fnexample(): (int, str) {return (32, "yo"); // expression that creates a tuple}
Array/Vector Construction
fnexample(): vec {return [true, "hows it going?", 42];}
Index/At
fnexample() {let vector = [3, 4, 5, 6];let val = vector[1]; // calls "at" - vector.at(1) is equivalent}
Range Constructor
<start (inclusive)>..<end (exclusive)> | <step>?
#[test]fnbasic_range() {let range =0..5;assertEq(range, [0, 1, 2, 3, 4]);}#[test]fnstep_range() {let range =4..11|3;assertEq(range, [4, 7, 10]);}#[test]fnbackwards() {let range =24..19;assertEq(range, [24, 23, 22, 21, 20]);}#[test]fnnegative_step() {let range =0..5|-2;assertEq(range, [4, 2, 0]);}#[test]fnbackwards_negative_step() {let range =20..10|-3;assertEq(range, [11, 14, 17, 20]);}
Type Of
Stof's "typeof" expression is handy for determining the type of a value programmatically, especially for "unknown" types and fields. It will always return the base type, one of: str, bool, obj, fn, blob, int, float, null, vec, map, set, or Box<type>. Numbers that are a unit type will result in float.
Stof's "typename" expression is similar to "typeof", however, will never return a box type ("Box<type>") and will always give the more complex type if present. For objects, this would be the custom type given to it, or "obj". For numbers that have units, the unit will be returned.
This is how one programmatically creates new objects in the document. Objects created will have the function's scope as its parent.
This does not create a field in the document pointing to the new object. If you don't want to lose your object, create a field for it with an assignment statement (shown below)!
Unless a field is explicitly created for newly created objects, they will not be exported via the standard formats.
Newly created objects are not dropped or reference counted automatically, so keep this in mind, especially for loops, and clean up your document responsibly. For most practical situations, this is not an issue.
Data on the other hand keeps track of the objects it is attached to, so dropping an object will remove data from the document only if that data isn't also referenced elsewhere.
typeCustomType { field:str;}#[test]fntest() {let object = new { // creates a new anonymous object in this document field:"hello"// initializes fields on this new object };let custom = new CustomType { field:"This is now a CustomType object" };let custom1:CustomType= new { field:"Casting this new object via type specifier on declaration" };let custom2 = new { field:"Casting this new object via 'as' keyword" } asCustomType; self.myobj = custom2; // creates a field on this object with our new object value}
#[test]fntest() {// Example of how to clean up after temporary objects that you// do not intend to keep around (reference from a field, etc.)for (_ in100) {// Create a new objectlet object = new { mydata:"some field" };assert(self.children().len() > 0);// Do something cool with it...// When done, drop it// Removes sub-objects and data (fields, funcs, etc.) that aren't referenced// by other objects in the document. drop object; }assertEq(self.children().len(), 0);}
Logical And & Or
Logical operations get executed from left to right, with parenthesis taking precedence.
#[test]fntest() {let first =true;let second =false;// ANDif (first && second) {// do something when both expressions evaluate to a truthy value// if "first" evaluates to falsy, "second" is not evaluated/checked }// ORif (first || second) {// do something if either expression evaluates to a truthy value// if "first" evaluates to truthy, "second" is not evaluated/checked }}
Ternary Expression
The ternary expression in Stof is slightly modified from other programming languages, starting with an "if" keyword. If the conditional expression is evaluated to have a truthy value, the first expression after the "?" is executed and returned, otherwise, the expression after the colon ":" is returned.
#[test]fnexample() {let x =iftrue?10:20;assertEq(x, 10); x =iffalse?10:20;assertEq(x, 20);}
Comparison Operations
#[test]fngreater() {assert(56>10);assertNot(10>10);}#[test]fngreaterOrEqual() {assert(10>=10);assert(20>=10);assertNot(10>=20);}#[test]fnless() {assert(10<20);assertNot(20<10);}#[test]fnlessOrEqual() {assert(10<=10);assert(10<=20);assertNot(20<=10);}#[test]fnequals() {assert(10==10);assertNot(10==11);}#[test]fnnotEquals() {assert(10!=20);assertNot(10!=10);}#[test]fnnotOperator() {let val =false;assert(!val);}