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:

EscapeByte
\nnewline
\ttab
\rcarriage return
\0zero byte
\\backslash
\"double quote
\xHHthe 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 at a/b/c.
  • mypkg = (#import "dir/file.sp") imports a single Spore file at dir/file.sp.

Math

  • (#add 1000 1000) performs modular arithmetic on integers of the same type. 1000 has type (#Array Bit 10) naturally.

Types

  • #Bit the 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)
  • #AnyType a type capable of holding instances of any type.
  • #AnyValue a type capable of holding instances of any value (that's everything).
  • #AnyProg a type capable of holding any program.

Errors

  • (#panic value) faults the MVM.
  • result = (#brace thingThatCouldPanic arg1 arg2). Returns a (#Sum Output #AnyValue) if thingThatCouldPanic has return type Output.
  • (#check expr) panics if expr is not equal to 1.
  • (#check expr1 expr2) panics if expr1 and expr2 are not equal.

Store

  • ref = (#post value) turns any T into a (#Ref T).
  • value = (#load ref) turns any (#Ref T) into a T.

Miscellaneous

  • (#=> data proc) is the same as (proc data)
  • 'expr quotes the next value: it evaluates to an #AST value instead of compiling the expression.
  • #AST is the type of a quoted AST node.
  • (#compileAST ast) compiles a compile-time #AST at the call site, so it resolves names in the call site's scope.
  • (#TypeOfEval ast) returns the AnyType that (#compileAST ast) would produce.
  • (#TypeOf 123) returns the type of 123, which is B7, 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 into body.
  • X = (#macro fn) defines a function macro: fn takes #AST arguments 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
})