The Spore Programming Language
Spore is a small programming language which compiles to Mycelium. WBS uses Spore as the configuration language.
Spore is designed for simplicity; it is a smaller language than Python or even Go. This article gives a complete tour of Spore. It resembles what an engineer would write on a whiteboard when outlining a program.
Spore is a compiled, typed language with immutable values.
Symbols & Variables
Spore has immutable variables, which you can bind with =.
Variables must be bound to a symbol.
Symbols must start with a letter and can contain alphanumerics and _.
Assign a number to myVar.
myVar = 1234
Literals
Bits
A bit is the unit of information.
There are two possible values 0 and 1.
firstBit = 0
secondBit = 1
The natural type of the numerals 0 and 1 is Bit, but they can be coerced to other types as well.
Numbers
We have already seen numbers used in the previous examples.
Spore's numbers are coerced to (#Array Bit _) where _ is some value of type (#Array Bit 32) aka B32.
Numbers must start with - or a numeral.
Underscores are allowed for readability
aMillion = 1_000_000
Hexadecimal can be specified with 0x
sixteen = 0x10
Binary can be specified with 0b
eight = 0b1000
The preamble to the compiler (included before all code sent for compilation) includes a few basic integer types, to which these numeral literals can be coerced.
B8
B16
B32
B64
B128
B256
You can define an integer type of any bitwidth
(#Array Bit 4096)
The natural type of a numeral is the smallest bit array that can represent it.
For example 31 would have type (#Array #Bit 5)
Strings
Strings are literal values of type List[Byte].
Double-quoted strings interpret escape sequences:
| Escape | Byte |
|---|---|
\n | newline |
\t | tab |
\r | carriage return |
\0 | zero byte |
\\ | backslash |
\" | double quote |
\xHH | the byte with hex value HH |
aString = "this is a string"
aTabbed = "col1\tcol2"
Backtick strings take everything between the backticks literally, newlines, backslashes, and double quotes included. They cannot contain a backtick.
aRawString = `C:\path\to\file`
aMultiline = `set -e
echo hello
`
Arrays
Spore Arrays are ordered, finite sequences of elements, which are all the same type. They can be coerced to Mycelium Arrays or Lists.
Lists are delimited with [ and ].
The elements are separated with a comma.
A trailing comma is tolerated and not required.
aListOfStrings = ["a", "b", "c"]
Tuples
A Tuple is an ordered, finite sequence of elements, each of an independent and not necessarily distinct type. Tuples compile to Mycelium Products.
Tuples are delimited with { and }.
The elements are separated with a comma.
A trailing comma is tolerated and not required.
aTuple = {"a", 1, "b", 2}
Lookup Table
Lookup Tables (LUT) are the primitive unit of computation in Spore. They look like match/switch statements in other languages. A LUT is a mapping from one set of values to another.
LUTs are delimited by { and }.
Inside are comma separated cases.
A case is a pair joined by =>.
{
0 => "zero",
1 => "one",
}
The natural type of a LUT is a Proc type, which takes the type of all the things to the left of the =>, and returns a value of the same type as the things to the right.
If a common type cannot be inferred, then a type must be specified.
((#Proc B2 String) {
0 => "a",
1 => "b",
2 => "c",
3 => "d",
})
All LUT cases must be mutually exclusive.
_ can be used once per LUT as a catch-all.
myLookTableProc = ((#Proc String B2) {
"a" => 1,
"b" => 2,
"c" => 3,
_ => 0,
})
Lambdas
Lambdas represent procedures.
Lambdas start with \, which is followed by a list of arguments, a return type, and then an expression.
This lambda takes a String and returns a String which repeats it 3 times.
repeatThrice = \(a: String) String {
(#flatten [a, a, a])
}
Lazies
Lazy values are delayed computations which require no additional input. They are specified like Lambdas, but without the arguments.
thisWillBeA4 = \B32 {
two = (B32 2)
(#add two two)
}
Content-ID (CID)
A CID is the hash of some data.
CIDs are often long, and copy-pasted rather than written by humans.
They start with an @ and contain hex characters after that.
CIDs take up 256 bits.
ref = ((#Ref (#Run Byte)) @deadbeef)
As a special case, 160-bit values are padded with zeros to 256 bits. This is to accommodate SHA1 hashes.
S-Expressions
In Spore, computation is expressed by enclosing a function and an input to that function together in parenthesis.
The whole thing, from ( to the corresponding ), is called an S-Expression, or SExpr for short.
Apply a Procedure
twice = \(x: B32) B32 (#add x x)
(twice 50)
evaluates to 100 :: B32
The first element of the SExpr is evaluated first. If it is a procedure, then that procedure is applied to a product created from the rest of the elements of the SExpr.
Eval a Lazy
An SExpr with a single element will evaluate a Lazy value.
lazy = \B32 (#add 1 2)
(lazy)
evaluates to 3 :: B32
Type an Expression
You can force any expression to be a certain type by having a type in the first position of the SExpr.
tenAsB4Naturally = 10
tenAsB32 = ((#Array Bit 32) 10)
Both of these are fine
3array = ((#Array String 3) ["a", "b", "c"])
list = ((#List String) ["a", "b", "c"])
Access a Field
A Tuple is like a function from indices to values.
aTuple = {"a", 2, \B32 4}
(#check (aTuple 0) "a")
(#check (aTuple 1) 2)
(#check ((aTuple 2)) 4)
Primitives
Primitives are operations exposed by the compiler.
They are defined in the compiler's source, not anywhere in the Spore source.
A Primitive always starts with #, followed by alphanumerics and symbols.
Importing
mypkg = (#import "a/b/c")imports a directory of Spore files ata/b/c.mypkg = (#import "dir/file.sp")imports a single Spore file atdir/file.sp.
Math
(#add 1000 1000)performs modular arithmetic on integers of the same type.1000has type(#Array Bit 10)naturally.
Types
#Bitthe bit type.(#Array ElementType len)evaluates to a new Array type.(#List ElementType)evaluates to a new List type.(#Product TypeA TypeB TypeC)(#Sum TypeA TypeB TypeC)(#Proc Input Output)(#Lazy Output)(#Ref ElementType)(#Distinct BaseType anyValue)#AnyTypea type capable of holding instances of any type.#AnyValuea type capable of holding instances of any value (that's everything).#AnyProga type capable of holding any program.
Errors
(#panic value)faults the MVM.result = (#brace thingThatCouldPanic arg1 arg2). Returns a(#Sum Output #AnyValue)ifthingThatCouldPanichas return typeOutput.(#check expr)panics if expr is not equal to 1.(#check expr1 expr2)panics ifexpr1andexpr2are not equal.
Store
ref = (#post value)turns anyTinto a(#Ref T).value = (#load ref)turns any(#Ref T)into aT.
Miscellaneous
(#=> data proc)is the same as(proc data)'exprquotes the next value: it evaluates to an#ASTvalue instead of compiling the expression.#ASTis the type of a quoted AST node.(#compileAST ast)compiles a compile-time#ASTat the call site, so it resolves names in the call site's scope.(#TypeOfEval ast)returns theAnyTypethat(#compileAST ast)would produce.(#TypeOf 123)returns the type of123, which isB7, without evaluating the expression.(#TypeOf (#post "abc"))=>(#Ref String)and does not evaluate the expression."abc"will not be posted to the store.(#anyValue "a string")creates an AnyValue wrapper containing "a string"(#anyType #Bit)creates an AnyType wrapper containing(#Bit)(#unwrapAnyValue Type anyValue)panics if anyValue does not contain a value of Type and evaluates to an AnyValue(#unwrapAnyType Type2 anyType)panics if anyType does not contain a value (it will also be a Type) of Type2
Macros
X = (#fnr (params) body)defines a find-and-replace macro:(X a b)substitutes the argument forms intobody.X = (#macro fn)defines a function macro:fntakes#ASTarguments and returns#AST, and(X a b)evaluates it at compile time and compiles the AST it returns.
Namespaces
Symbols in Spore are used to identify values in a namespace.
The current local namespace's symbols can always be used directly, as we have seen previously.
To access symbols in other namespaces, like an imported namespace, use the . operator.
Symbol within Import
bits = (#import "abc")
(#check (bits.xor 0 1) 1)
(#check (bits.xor 1 1) 0)
Exporting Symbols
Symbols can be exported from the current namespace, for import into another namespace, by prefixing them with ..
localOnly = "abc123"
.exported = "exported"
Namespaced Types
The #Struct and #TaggedUnion types:
myStructType = (#Struct {
.fieldA = B32
.fieldB = String
})
myStructValue = (myStructType {
.fieldA = 1234
tmp1 = "foo"
tmp2 = "bar"
.fieldB = (#flatten [tmp1 tmp2])
})
myTaggedUnionType = (#TaggedUnion {
.fieldA = B32
.fieldB = String
})
myTaggedUnionValue1 = (myTaggedUnionType {
.fieldB = "foobar"
// also providing .fieldA is not allowed
})
myTaggedUnionValue2 = (myTaggedUnionType {
.fieldA = 12345
// also providing .fieldB is not allowed
})