Sire is the default language for Pallas and is bootstrapped directly from PLAN. It is also possible to compile mainstream functional languages to PLAN. At that point, much Pallas development—including full-stack web applications—will be accomplished by writing in those languages only, leaving Sire responsible for code that needs to be optimized by the runtime (like new crypto libraries, say).
Open up a bootstrapped Sire REPL. As a reminder:
nix develop
pallas sire sire/prelude.sire
Or if you're using the docker images:
docker pull deathtothecorporation/sire-repl
docker run -it deathtothecorporation/sire-repl
We're going to hands-on learn a few basic concepts: Top-level bindings, let bindings and function application.
A few meta-points to note at the outset:
;
is a comment in Sire. used here, it'll be to explain what's happening inline- The arrow keys won't work in the REPL. Use backspace. If you get into a strange state with carriage returns and are unable to backspace, just hit return a few times, ignore any errors, and start again.
- The REPL will print ASCII values of integers. If you enter
65
you'll get back%A
. Don't think about this too much yet. We'll cover this in detail on the next page. - There is no input prompt in the REPL. And the result of the previous computation (after hitting
enter
) is just printed out on the next line. So if you're only reading and not following along live in the REPL, generally the first line of text you see in a code block is the user input, and the lines following it is the result of hitting theenter
key. - The single-character "symbolic operators" you'll see here are referred to as runes. They are core syntactic elements that perform various functions in Sire, like function application and data structure definition.
Binds a value globally - not scoped.
In the REPL:
; On the blank line in the REPL, type the following:
x=3
; And press enter.
; Now just type "x"...
x
; And press enter.
3 ; <-- the value returned from the REPL
add
is a function in Sire. It takes two arguments and adds them together. We apply add
to 1
and 3
like so:
| add 1 3
; ^ function name (add)
; ^ first argument (1)
; ^ second argument (3)
4 ; return value
You can also apply functions with ()
:
(add 1 3)
4
This is actually a short form for (| add 1 3)
- we're still doing function application with |
under the hood, but the |
rune is a special case that can be omitted in this context. All runes can be written several different ways (as you'll see next with the =
rune).
Let's combine both of the above concepts to create our own named function. Note the way in this case you start the line with =
(followed by a space) and the name of the function is the first value after the opening parenthesis.
; We're using the prefix notation for = here, which we saw in "infix" notation above at x=3
= (addTwo input)
| add 2 input
The REPL now has a function named addTwo
bound to its top-level scope. The function takes a single value (called input
) and all it does is apply (with |
) the add
function to two arguments, a hard-coded 2
and whatever input was provided.
(addTwo 4)
6 ; return value
@
binds a value to the present scope.
We can see this by trivially modifying our addTwo
function to bind an arbitrary name and use that for the addition:
= (addTwo originalInput)
@ renamedInput originalInput
| add 2 renamedInput
(addTwo 4)
6
On the second line, we bound a new name, renamedInput
with the value of originalInput
. Then 2
is added to it.
To prove to yourself that renamedInput
is only bound within the function scope, try calling it at the top-level of the REPL:
renamedInput
++ %crash
++ {Failed to Parse Sire}
++ ` # block renamedInput
# where REPL:214
# problem renamedInput
# reason
{++ {Failed to Parse Sire}
++ ` # block renamedInput
# where REPL:214
# problem renamedInput
# reason {Unresolved symbol: renamedInput}
All values in Pallas are trees, whose leaves are natural numbers. Throughout the system, these are referred to as "nats". The REPL will represent these values as ASCII for printing purposes, which can be confusing at first and should be noted up front. For instance, the character a
is encoded as 97
in ASCII:
97
;; prints:
%a
| showNat %a
;; prints:
{97}
;; The showNat function represents a nat as a string.
;; Single curly braces wrapping a value means you're seeing a string.
;; Think of {curly braces} as you would "double quotes", as far as the
;; REPL is concerned. Same for the % leading the "a" a few lines above.
The full explanation is of how nats are rendered is here, but we suggest you continue along for now without getting too bogged down in the REPL representation of nats.
A "bar" is an array of UTF-8 bytes. You create a bar like this:
b#{some stuff here}
You only need to use the curly braces when spaces are present in your byte array:
b#thisIsAFineBar
While it may be attractive to think about bars and strings interchangeably, they are not identical and when working between the two you'll often need to use conversion functions (like natBar and barNat. There are more standard library functions for working with bars on a structural level (folding, splitting at indexes, filtering, etc.) while the string standard library functions are more geared to character-level functions like capitalization and checking if a character is an alphanumeric.
Trust us that you can and should basically just use bars for everything string-like and you can move along to the section on data structures below, but if you're interested in seeing the deep dive, it's here.
Rows/vectors are basically arrays (not Lists - see below). They are defined with [ ]
:
arr=[10 64 42]
arr
[10 64 42]
We'll get into more of the standard library/convenience functions later, but we'll need a few now, too, in order to prove to ourselves some details of rows. idx
is used to get the value at a given index in a row:
arr=[10 64 42]
(idx 0 arr)
10
; the zeroth item in the row
(idx 2 arr)
42
(idx 50 arr)
0
; you'll get zero back if you overshoot
len
will give you the size of the row:
(len arr)
3
Lists are zero-terminated, nested row 2-tuples. They are declared by prepending a ~
to what looks like row syntax, like this: ~[]
(in the REPL we have to wrap this in parentheses):
x=(~[10 64 42])
; Note, parentheses are added around the list because of the infix =
; You could also write this as: = x ~[10 64 42]
; The choice between infix and prefix is most often one of visual clarity.
[10 [64 [42 0]]]
(idx 0 x)
10
(idx 1 x)
[64 [42 0]]
Incidentally, you can create a list from a row with the listFromRow
function:
y=[2 3 4]
listy=(listFromRow y)
listy
[2 [3 [4 0]]]
TRUE
is represented as the nat 1
, while FALSE
is the nat 0
.
The =?=
operator is a unit test. It's scope is the same as a top level definition and so it cannot be used within a function.
It takes two arguments and exists source file loading if they are not equal, otherwise it evaluates without issue.
=?= 1 TRUE
=?= 0 FALSE
; These two expressions evaluate without issue.
=?= 1 FALSE
; This one crashes:
++ %crash
++ {Failed to Parse Sire}
++ ` # block
=?= 1 | FALSE
# where REPL:26
# problem
=?= 1 | FALSE
# reason
{++ {Failed to Parse Sire}
++ ` # block
=?= 1 | FALSE
# where REPL:26
# problem
=?=
* 1
* 0
# reason {ASSERTION FAILURE}
The eql
function takes two parameters and returns TRUE
if they're equal)
eql 1 1
1
eql 1 999
0
Similarly, we have neq
for "not equal". Also:
lth
- true when first parameter is less than secondlte
- true when first parameter is less then or equal to secondgth
- true when first parameter is greater than secondgte
- true when first parameter is greater than or equal to second
if
is a function with three parameters: what is being tested for truthiness; what to return if true; what to return if false.
| if 1 {the if got a true} {the if got a false}
; returns:
{the if got a true}
The if
function's first parameter was 1
, a true value, so the second parameter was returned.
Negation.
| not FALSE
; returns:
1
and
and or
are functions that do what you'd expect:
and 1 1
;; returns:
1
and 1 0
;; returns:
0
or 1 0
;; returns:
1
or 0 0
;; returns:
0