TUTORIAL

BEFORE YOU START

This tutorial is for people who have written programs before, in any language. It moves quickly through ten lessons, from hello world to threads, and every example on this page has been run through satl, the SATELLITE interpreter. What you see under each program is exactly what it prints.

If you have never programmed, start with BEGINNERS instead. If you want every word of the language in one place, that is the FULL REFERENCE. To get satl on your computer, see INSTALL.

To try an example, save it under the file name shown above it, then run it:

BASH Download
satl hello.satl

Each lesson ends with an exercise. Try it before you read the answer below it.

LESSON 1: HELLO, WORLD

Here is a complete SATELLITE program.

hello.satlSATELLITE 004 Download
The smallest complete program
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.console.display("Hello, World!")
    satellite.return(satellite)
}
OUTPUT
Hello, World!

Every program you run needs three things, and satl checks for all three before anything runs:

  • satellite.include(satellite) says this file is a program.
  • satellite.capsule satellite.main() is where it begins. A capsule is SATELLITE’s word for a function.
  • satellite.return(satellite) is where it ends. Its place is the last line of main.

Leave one out and satl tells you which one is missing and what to type.

The one rule behind every name

Look at the names in that program. Every one of them starts with satellite., and that is the most important rule in the language: a dotted path that starts with satellite names something the language owns, and a bare name is something you own. satellite.console.display is the language’s word for writing a line. When you write apples or greet or account, that is yours. You can always tell at a glance which is which.

How a program is laid out

  • One statement is one line. There are no semicolons.
  • Braces go on lines of their own after an if, a while or a for, as in every example here.
  • // starts a comment that runs to the end of the line.
  • satl writes its own title block (the language’s name, version and build) to standard error before your program starts. Only what your program displays goes to standard output, so you can pipe it or send it to a file.

satellite.console.display writes one value and then ends the line. Give it end= to end the line with something else, or with nothing at all. It also takes foreground=, background=, bold= and italic= for colour and style; see DISPLAYING TEXT and COLOR ON THE CONSOLE.

loading.satlSATELLITE 004 Download
end= keeps the next display on the same line
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.console.display("Loading", end="...")
    satellite.console.display(" done")
    satellite.console.display("careful", foreground=xFF8800, bold=satellite.bool.true)
    satellite.return(satellite)
}
OUTPUT
Loading... done
careful

At a terminal, careful shows in bold orange. Into a pipe or a file the colour is left out, which is why it prints plain here.

satl --repl opens the satl prompt, where you can type one line and see it run straight away. It is a good place to try a single display, such as satellite.console.display(2 ^ 100). For every option the command takes, see THE SATL COMMAND.

Exercise 1

Write countdown.satl, which prints 3... 2... 1... liftoff! on a single line, using four displays.

Answer 1

countdown.satlSATELLITE 004 Download
Four displays, one line
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.console.display("3", end="... ")
    satellite.console.display("2", end="... ")
    satellite.console.display("1", end="... ")
    satellite.console.display("liftoff!")
    satellite.return(satellite)
}
OUTPUT
3... 2... 1... liftoff!

More on this lesson: YOUR FIRST SATELLITE PROGRAM.

LESSON 2: VALUES AND TYPES

A variable is declared with its type, then its name, then its value. The type is the language’s, so it starts with satellite.. The name is yours.

values.satlSATELLITE 004 Download
Numbers, floats, strings and bools
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.number apples = 7
    satellite.variable.number people = 2
    satellite.console.display(apples / people)
    satellite.console.display(apples % people)
    satellite.console.display(2 ^ 100)

    satellite.variable.float price = 19.99
    satellite.console.display(price * 3)
    satellite.console.display(7.0 / 2)
    satellite.console.display(0.1 + 0.2 == 0.3)

    satellite.variable.string name = "Ada"
    satellite.console.display("Hello, " + name + "!")
    satellite.console.display("apples: " + apples.to_string())
    satellite.console.display(name.reverse())

    satellite.variable.bool enough = apples > people
    satellite.console.display(enough)
    satellite.return(satellite)
}
OUTPUT
3
1
1267650600228229401496703205376
59.97
3.5
true
Hello, Ada!
apples: 7
adA
true

What to notice:

  • satellite.variable.number is a whole number with no size limit but your memory. It never wraps around, so 2 ^ 100 is exact. ^ is power.
  • / between two whole numbers is whole-number division, so 7 / 2 is 3. Write 7.0 / 2 when you want 3.5.
  • satellite.variable.float is decimal all the way down, so 0.1 + 0.2 == 0.3 is true.
  • Every operator is written with a space on both sides. n+3 is refused, and so is n += 1: write n = n + 1.
  • A number added to text joins it as its digits: "apples: " + apples prints apples: 5, and apples + " apples" prints 5 apples. Text that spells a number is added instead: 4 + "2" is the number 6. Going the other way, "42".number reads a number out of text.
  • A name keeps its type. A number name refuses a string, and a string name refuses a number.
  • satellite.variable.bool holds satellite.bool.true or satellite.bool.false, and every comparison answers one.

SATELLITE has more kinds of value than most languages, each written the way you would read it:

more_values.satlSATELLITE 004 Download
Percentages, binary, hex, colours, fractions and infinity
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.percent tip = 15%
    satellite.console.display(80 * tip)
    satellite.console.display(80 + tip)

    satellite.variable.binary flags = b0110
    satellite.console.display(flags)
    satellite.console.display(flags.number)

    satellite.variable.hex mask = x00FF
    satellite.console.display(mask.number)

    satellite.variable.color sky = x87CEEB
    satellite.console.display(sky)

    satellite.variable.fraction third = 1/3
    satellite.console.display(third < 1/2)

    satellite.variable.infinity big = satellite.infinity()
    satellite.console.display(big > 10 ^ 100)
    satellite.return(satellite)
}
OUTPUT
12
92
b0110
6
255
x87CEEB
true
true

80 + tip is 80 grown by 15%. A binary keeps the width you wrote it with, so b0110 shows its leading zero. A fraction is written with a touching slash, 1/3; with spaces, 1 / 3 is division. Each of these has a post of its own: NUMBERS, FLOATS, STRINGS, PERCENTAGES, BINARY AND HEXADECIMAL, COLORS, FRACTIONS, INFINITY and OPERATORS.

Exercise 2

A bill comes to 80 and you want to leave a 15% tip. Write tip.satl, which prints tip: 12 and then total: 92. Keep each amount in a number variable and convert it for the sentence.

Answer 2

tip.satlSATELLITE 004 Download
A percentage of a number, and back into text
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.number bill = 80
    satellite.variable.percent rate = 15%
    satellite.variable.number tip = bill * rate
    satellite.variable.number total = bill + rate
    satellite.console.display("tip: " + tip.to_string())
    satellite.console.display("total: " + total.to_string())
    satellite.return(satellite)
}
OUTPUT
tip: 12
total: 92

LESSON 3: CAPSULES

A capsule is a function. Its parameters are written the way every declaration is, type then name. satellite.return(value) hands the answer back, from wherever it stands.

capsules.satlSATELLITE 004 Download
Parameters, an answer, and a capsule declared after main
satellite.include(satellite)

satellite.capsule satellite.main()
{
    greet("world", 2)
    satellite.variable.number n = twice(21)
    satellite.console.display(n)
    satellite.console.display(sign_of(-5))
    satellite.return(satellite)
}

satellite.capsule greet(satellite.variable.string who, satellite.variable.number times)
{
    satellite.variable.number i = 0
    satellite.statement.while (i < times)
    {
        satellite.console.display("hello, " + who)
        i = i + 1
    }
}

satellite.capsule twice(satellite.variable.number n)
{
    satellite.return(n * 2)
}

satellite.capsule sign_of(satellite.variable.number n)
{
    satellite.statement.if (n < 0)
    {
        satellite.return("below zero")
    }
    satellite.return("zero or above")
}
OUTPUT
hello, world
hello, world
42
below zero

What to notice:

  • The order capsules are declared in does not matter. main can come first.
  • There are no globals in SATELLITE. A capsule runs in a frame of its own and cannot see the caller’s variables. What you hand it is all it has.
  • Each argument is checked against its parameter’s type when the call is made. A float parameter wants a float, so pass 100.0, not 100.
  • A capsule that answers nothing can leave out satellite.return; reaching its closing brace ends it.

Namespaces and library values

Group capsules with satellite.namespace and call them through its name. When every capsule needs the same fixed value, write it once at the top of the file as satellite.library.name = value. Library values are written down, never worked out, and nothing can change them, so they are settings, not globals.

temperatures.satlSATELLITE 004 Download
A namespace and a library value
satellite.include(satellite)

satellite.library.freezing = 32.0

satellite.namespace convert
{
    satellite.capsule to_fahrenheit(satellite.variable.float celsius)
    {
        satellite.return(celsius * 9 / 5 + satellite.library.freezing)
    }
}

satellite.capsule satellite.main()
{
    satellite.console.display(convert.to_fahrenheit(100.0))
    satellite.console.display(convert.to_fahrenheit(37.5))
    satellite.console.display(convert.to_fahrenheit(-40.0))
    satellite.return(satellite)
}
OUTPUT
212.0
99.5
-40.0

Every file is a namespace too. satellite.include("shapes") at the top of your program lets you call that file’s capsules as shapes.area(2, 3). See INCLUDING FILES AND NAMESPACES and SATELLITE.LIBRARY.

The words your program was started with

main may take one parameter: the arguments variable. It holds the words after your file name on the command line, as args.argument_1, args.argument_2 and so on, and args.length counts them with the program itself included. The name args is yours to choose.

greet.satlSATELLITE 004 Download
Reading the command line
satellite.include(satellite)

satellite.capsule satellite.main(satellite.variable.arguments args)
{
    satellite.statement.if (args.length > 1)
    {
        satellite.console.display("hello, " + args.argument_1)
    }
    satellite.statement.else
    {
        satellite.console.display("hello, whoever you are")
    }
    satellite.return(satellite)
}
OUTPUT
hello, whoever you are

Run with a word after the file name, it greets that word:

BASH Download
satl greet.satl Ada
TEXT Download
hello, Ada

The same variable carries satl’s settings and facts about your machine. See THE ARGUMENTS VARIABLE.

Exercise 3

Write gcd(a, b), the greatest common divisor, by Euclid’s rule: when b is 0 the answer is a, and otherwise it is gcd(b, a % b). Print gcd(48, 18) and gcd(1071, 462).

Answer 3

A capsule may call itself. Here the call sits inside satellite.return(...), so it is an ordinary call: each level waits for the one below it to answer. Euclid’s rule needs only a few dozen levels even for very large numbers, so that is no trouble. When a capsule must go very deep, make its call to itself a line of its own and its last statement; satl runs that shape as a loop, however deep it goes.

gcd.satlSATELLITE 004 Download
A capsule that calls itself
satellite.include(satellite)

satellite.capsule gcd(satellite.variable.number a, satellite.variable.number b)
{
    satellite.statement.if (b == 0)
    {
        satellite.return(a)
    }
    satellite.return(gcd(b, a % b))
}

satellite.capsule satellite.main()
{
    satellite.console.display(gcd(48, 18))
    satellite.console.display(gcd(1071, 462))
    satellite.console.display(gcd(2 ^ 80, 6 ^ 20))
    satellite.return(satellite)
}
OUTPUT
6
21
1048576

More on this lesson: CAPSULES.

LESSON 4: STATEMENTS AND LOOPS

SATELLITE has three statements for making decisions and repeating work: satellite.statement.if (with satellite.statement.else), satellite.statement.while and satellite.statement.for.

loops.satlSATELLITE 004 Download
if and else, while, and three kinds of for step
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.number temperature = 18
    satellite.statement.if (temperature > 25)
    {
        satellite.console.display("hot")
    }
    satellite.statement.else satellite.statement.if (temperature > 15)
    {
        satellite.console.display("mild")
    }
    satellite.statement.else
    {
        satellite.console.display("cold")
    }

    satellite.variable.number fuel = 10
    satellite.statement.while (fuel > 0)
    {
        fuel = fuel - 3
    }
    satellite.console.display(fuel)

    satellite.statement.for(satellite.variable.number i = 3; i >= 1; i--)
    {
        satellite.console.display(i)
    }
    satellite.statement.for(satellite.variable.number i = 0; i <= 10; i + 5)
    {
        satellite.console.display(i)
    }
    satellite.return(satellite)
}
OUTPUT
mild
-2
3
2
1
0
5
10

What to notice:

  • A for has three parts separated by semicolons: a declaration, a condition, and a step. The step is i++, i--, or the counter and one operator with a space on both sides, such as i + 5 or i * 2. You do not write i = in the step.
  • The counter belongs to the loop. Using i after the loop has ended is refused.
  • The body of an if or a loop shares the variables around it, so fuel changed inside the while is still changed after it.
  • A condition is one comparison today. && and || are not built, so put one if inside another, or chain else if as above.
  • There is no break. To leave a loop early, put the loop in a capsule and satellite.return from inside it:
first_over.satlSATELLITE 004 Download
Leaving a loop early by returning from its capsule
satellite.include(satellite)

satellite.capsule first_over(satellite.container.list<satellite.variable.number> numbers, satellite.variable.number limit)
{
    satellite.statement.for(satellite.variable.number i = 1; i <= numbers.size; i++)
    {
        satellite.statement.if (numbers[i] > limit)
        {
            satellite.return(numbers[i])
        }
    }
    satellite.return(0)
}

satellite.capsule satellite.main()
{
    satellite.console.display(first_over({72, 95, 88, 99}, 90))
    satellite.console.display(first_over({1, 2, 3}, 90))
    satellite.return(satellite)
}
OUTPUT
95
0

Exercise 4

The classic: print the numbers from 1 to 15, but print Fizz for multiples of 3, Buzz for multiples of 5, and FizzBuzz for multiples of both.

Answer 4

fizzbuzz.satlSATELLITE 004 Download
FizzBuzz with an else-if chain
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.statement.for(satellite.variable.number i = 1; i <= 15; i++)
    {
        satellite.statement.if (i % 15 == 0)
        {
            satellite.console.display("FizzBuzz")
        }
        satellite.statement.else satellite.statement.if (i % 3 == 0)
        {
            satellite.console.display("Fizz")
        }
        satellite.statement.else satellite.statement.if (i % 5 == 0)
        {
            satellite.console.display("Buzz")
        }
        satellite.statement.else
        {
            satellite.console.display(i)
        }
    }
    satellite.return(satellite)
}
OUTPUT
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

More on this lesson: IF, WHILE AND FOR.

LESSON 5: LISTS AND INDEXES

A list is written with braces anywhere a value goes. satellite.container.list<type> checks every item that goes in; with no <> a list holds anything. Lists count from 1, the way the lines of a file do.

lists.satlSATELLITE 004 Download
A list: reading, changing, sorting, adding up
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.container.list<satellite.variable.number> scores = {72, 95, 88}
    scores.append(61)
    satellite.console.display(scores)
    satellite.console.display(scores[1])
    satellite.console.display(scores.size)
    satellite.console.display(scores.sum)
    satellite.console.display(scores.max)
    satellite.console.display(scores.contains(95))
    satellite.console.display(scores.sort().by_value())
    satellite.console.display(scores.sort().by_value().reverse())
    satellite.console.display(scores.join(" + "))

    satellite.container.list<satellite.variable.number> copy = scores
    copy[1] = 0
    satellite.console.display(scores[1])

    satellite.container.list<satellite.container.list<satellite.variable.string>> board = {{"X", "O", "X"}, {" ", "X", "O"}, {"O", " ", "X"}}
    board[2][1] = "O"
    satellite.console.display(board[2].join("|"))
    satellite.return(satellite)
}
OUTPUT
{72, 95, 88, 61}
72
4
316
95
true
{61, 72, 88, 95}
{95, 88, 72, 61}
72 + 95 + 88 + 61
72
O|X|O

What to notice:

  • .append, .insert(n, x), .remove(x), .remove_at(n), .remove_last() and .clear change the list. .sort(), .reverse(), .sum, .max, .min and .join answer a new value and leave the list alone.
  • .sort() is always followed by .by_value() (smallest first) or .by_name() (by how each item reads).
  • copy = scores copies the list, so changing the copy leaves scores alone. The copy costs nothing until one of them is written to.
  • board[2][1] reaches into a list of lists, to read or to write.
  • Reading past the end, or scores[0], stops the program with a report.
  • To walk a list, use for and a[i], as in the last lesson.

Indexes: keys and values

satellite.container.index<key, value> files values under keys, like a dictionary in Python. Its keys come back in the order you put them in. A key can be a number, a string, a bool, a binary or a percentage. (satellite.container.map is a second word for the same container, and a map can be written whole: {"zoe": 30, "al": 4}.)

ages.satlSATELLITE 004 Download
An index from names to numbers
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.container.index<satellite.variable.string, satellite.variable.number> age
    age["zoe"] = 30
    age["al"] = 4
    age["bo"] = 100
    age["zoe"] = 31
    satellite.console.display(age)
    satellite.console.display(age["al"])
    satellite.console.display(age.keys)
    satellite.console.display(age.values.sum)
    satellite.console.display(age.contains("bo"))
    age.remove("al")
    satellite.console.display(age.size)
    satellite.return(satellite)
}
OUTPUT
{"zoe": 31, "al": 4, "bo": 100}
4
{"zoe", "al", "bo"}
135
true
2

Reading a key that is not there stops the program rather than inventing an answer, so ask .contains(key) first when you are not sure.

Exercise 5

Here are the votes for the office drink: {"tea", "coffee", "tea", "water", "coffee", "tea", "juice"}. Count them into an index, print the index, then print the winner as tea wins with 3 votes.

Answer 5

votes.satlSATELLITE 004 Download
Counting into an index, then finding the largest
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.container.list<satellite.variable.string> votes = {"tea", "coffee", "tea", "water", "coffee", "tea", "juice"}
    satellite.container.index<satellite.variable.string, satellite.variable.number> counts
    satellite.statement.for(satellite.variable.number i = 1; i <= votes.size; i++)
    {
        satellite.variable.string drink = votes[i]
        satellite.statement.if (counts.contains(drink))
        {
            counts[drink] = counts[drink] + 1
        }
        satellite.statement.else
        {
            counts[drink] = 1
        }
    }
    satellite.console.display(counts)

    satellite.container.list<satellite.variable.string> drinks = counts.keys
    satellite.variable.string best = drinks[1]
    satellite.statement.for(satellite.variable.number i = 2; i <= drinks.size; i++)
    {
        satellite.statement.if (counts[drinks[i]] > counts[best])
        {
            best = drinks[i]
        }
    }
    satellite.console.display(best + " wins with " + counts[best].to_string() + " votes")
    satellite.return(satellite)
}
OUTPUT
{"tea": 3, "coffee": 2, "water": 1, "juice": 1}
tea wins with 3 votes

More on this lesson: LISTS, GRIDS: LISTS OF LISTS, MAPS and MULTIPLE, which lets one name hold any of several types.

LESSON 6: SPACESUITS

A spacesuit is SATELLITE’s class: a type you write yourself, with its own variables and capsules. (satellite.class is a second spelling of the same word.) It has three sections:

  • satellite.protected holds its fields, and any capsules only its own capsules may call.
  • satellite.constructor(...) runs when an object is made, after every field has its first value.
  • satellite.public holds the capsules anyone with an object may call.
account.satlSATELLITE 004 Download
A bank account, made by declaring it
satellite.include(satellite)

satellite.spacesuit account()
{
    satellite.protected
    {
        satellite.variable.string owner = ""
        satellite.variable.number balance = 0
    }

    satellite.constructor(satellite.variable.string who, satellite.variable.number opening)
    {
        owner = who
        balance = opening
    }

    satellite.public
    {
        satellite.capsule deposit(satellite.variable.number amount)
        {
            balance = balance + amount
        }

        satellite.capsule withdraw(satellite.variable.number amount)
        {
            satellite.statement.if (amount > balance)
            {
                satellite.return(satellite.bool.false)
            }
            balance = balance - amount
            satellite.return(satellite.bool.true)
        }

        satellite.capsule summary()
        {
            satellite.return(owner + " has " + balance.to_string())
        }
    }
}

satellite.capsule pay_rent(account from)
{
    from.withdraw(700)
}

satellite.capsule satellite.main()
{
    account ada("Ada", 1000)
    ada.deposit(250)
    satellite.console.display(ada.summary())
    satellite.console.display(ada.withdraw(5000))
    pay_rent(ada)
    satellite.console.display(ada.summary())
    account same = ada
    same.deposit(1)
    satellite.console.display(ada.summary())
    satellite.return(satellite)
}
OUTPUT
Ada has 1250
false
Ada has 550
Ada has 551

What to notice:

  • An object is made by declaring it: the spacesuit’s name, the object’s name, and the constructor’s arguments, as in account ada("Ada", 1000). With no constructor arguments it is just account ada.
  • Inside its capsules a spacesuit’s fields are their bare names. There is no this.
  • Fields are reached from inside the spacesuit only. ada.balance from main is refused before anything runs; write a public capsule that answers it.
  • An object is a reference. same = ada names the same account, and pay_rent changed the caller’s account. (Numbers, strings and lists are values, and are copied.)

One spacesuit extending another

Name another spacesuit in the brackets to extend it. The new one gets its fields and capsules, and a capsule it declares again replaces the old one for its objects, even when the old spacesuit’s own capsules call it.

pets.satlSATELLITE 004 Download
dog extends pet and replaces one capsule
satellite.include(satellite)

satellite.spacesuit pet()
{
    satellite.protected
    {
        satellite.variable.string name = ""
    }

    satellite.constructor(satellite.variable.string given)
    {
        name = given
    }

    satellite.public
    {
        satellite.capsule sound()
        {
            satellite.return("...")
        }

        satellite.capsule describe()
        {
            satellite.return(name + " says " + sound())
        }
    }
}

satellite.spacesuit dog(pet)
{
    satellite.public
    {
        satellite.capsule sound()
        {
            satellite.return("woof")
        }
    }
}

satellite.capsule satellite.main()
{
    pet goldie("Goldie")
    dog rex("Rex")
    satellite.container.list<pet> all = {goldie, rex}
    satellite.statement.for(satellite.variable.number i = 1; i <= all.size; i++)
    {
        satellite.console.display(all[i].describe())
    }
    satellite.return(satellite)
}
OUTPUT
Goldie says ...
Rex says woof

dog has no constructor of its own, so rex("Rex") hands its argument to pet‘s. A list of pet can hold a dog.

Exercise 6

Write a spacesuit stack that keeps strings in a list, with push(item), pop(), which answers the last item pushed and takes it off, and size(). Push three plates, then pop two.

Answer 6

stack.satlSATELLITE 004 Download
A stack built on a list field
satellite.include(satellite)

satellite.spacesuit stack()
{
    satellite.protected
    {
        satellite.container.list<satellite.variable.string> items = {}
    }

    satellite.public
    {
        satellite.capsule push(satellite.variable.string item)
        {
            items.append(item)
        }

        satellite.capsule pop()
        {
            satellite.variable.string top = items.last
            items.remove_last()
            satellite.return(top)
        }

        satellite.capsule size()
        {
            satellite.return(items.size)
        }
    }
}

satellite.capsule satellite.main()
{
    stack plates
    plates.push("blue")
    plates.push("green")
    plates.push("red")
    satellite.console.display(plates.size())
    satellite.console.display(plates.pop())
    satellite.console.display(plates.pop())
    satellite.console.display(plates.size())
    satellite.return(satellite)
}
OUTPUT
3
red
green
1

More on this lesson: SPACESUITS and ONE SPACESUIT EXTENDING ANOTHER.

LESSON 7: FILES

In SATELLITE a text file is a list of lines, counting from 1. satellite.file.new(path) makes a new file and satellite.file.open(path) opens one that is already there. Both answer a satellite.variable.file, and the file’s methods are spelled like a list’s.

shopping.satlSATELLITE 004 Download
Making a file, then opening, reading and changing it
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.console.display(satellite.file.exists("shopping.txt"))
    satellite.variable.file list = satellite.file.new("shopping.txt")
    list.append("bread")
    list.append("milk")
    list.append("apples")
    list.close()
    satellite.console.display(satellite.file.exists("shopping.txt"))

    satellite.variable.file again = satellite.file.open("shopping.txt")
    satellite.console.display(again[2])
    satellite.console.display(again.search("app"))
    again.replace(2, "oat milk")
    again.insert(1, "coffee")
    satellite.statement.for(satellite.variable.number i = 1; i <= again.size; i++)
    {
        satellite.console.display(i.to_string() + ". " + again[i])
    }

    satellite.variable.file twice = satellite.file.new("shopping.txt")
    satellite.console.display(twice.ok)
    satellite.return(satellite)
}
OUTPUT
false
true
milk
3
1. coffee
2. bread
3. oat milk
4. apples
false

What to notice:

  • f[n] reads line n as a string. .search(text) answers the number of the first line that contains the text, or 0. .index_of(text) finds a line that is exactly the text.
  • .append, .insert(n, text), .replace(n, text), .remove_at(n) and friends change lines. .replace("old", "new") given two strings changes text instead of a whole line.
  • new never replaces a file, and open never makes one. When either cannot do its job, the program carries on and the handle’s .ok is false; .error says why.
  • Changes reach the disk at .close() or .save, and whole or not at all. You cannot lose work by forgetting: when the program ends, satl saves and closes every file still open.
  • A path without a leading / is taken from the folder your program is in, not from where you ran satl.
  • Reading a line that is not there stops the program with a report, as a list does.

Exercise 7

Write the squares of 1 to 5 into squares.txt, one to a line, and close it. Then open it again, add up every line (each line is text, so read it back with .number), and print 5 lines, adding up to 55.

Answer 7

squares.satlSATELLITE 004 Download
Writing numbers as lines and reading them back
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.file out = satellite.file.new("squares.txt")
    satellite.statement.for(satellite.variable.number i = 1; i <= 5; i++)
    {
        satellite.variable.number square = i * i
        out.append(square.to_string())
    }
    out.close()

    satellite.variable.file back = satellite.file.open("squares.txt")
    satellite.variable.number total = 0
    satellite.statement.for(satellite.variable.number line = 1; line <= back.size; line++)
    {
        total = total + back[line].number
    }
    satellite.console.display(back.size.to_string() + " lines, adding up to " + total.to_string())
    satellite.return(satellite)
}
OUTPUT
5 lines, adding up to 55

More on this lesson: FILES.

LESSON 8: THREADS

A thread runs a capsule at the same time as the rest of your program. It takes three steps:

  1. satellite.thread.new(capsule(arguments)) works out the arguments now and makes the thread. It does not run the capsule yet.
  2. .start() runs the capsule on a thread of its own and returns at once.
  3. .join() waits for it to finish and answers what the capsule handed back. .wait() is a second name for .join().
halves.satlSATELLITE 004 Download
Adding up 1 to 100,000 in two halves at once
satellite.include(satellite)

satellite.capsule add_up(satellite.variable.number from, satellite.variable.number to)
{
    satellite.variable.number total = 0
    satellite.statement.for(satellite.variable.number i = from; i <= to; i++)
    {
        total = total + i
    }
    satellite.return(total)
}

satellite.capsule satellite.main()
{
    satellite.variable.thread low = satellite.thread.new(add_up(1, 50000))
    satellite.variable.thread high = satellite.thread.new(add_up(50001, 100000))
    satellite.console.display(low)
    low.start()
    high.start()
    satellite.variable.number total = low.join() + high.join()
    satellite.console.display(total)
    satellite.console.display(low)
    satellite.return(satellite)
}
OUTPUT
(thread add_up, not started)
5000050000
(thread add_up, finished)

Displaying a thread tells you which capsule it runs and how far it has got.

What a thread shares

Numbers, strings and lists are values: a thread gets its own copy. An object of a spacesuit is a reference, so a thread given one works on the same object as main.

sharing.satlSATELLITE 004 Download
A list is copied, an object is shared
satellite.include(satellite)

satellite.spacesuit box()
{
    satellite.protected
    {
        satellite.variable.number held = 0
    }

    satellite.public
    {
        satellite.capsule put(satellite.variable.number n)
        {
            held = n
        }

        satellite.capsule get()
        {
            satellite.return(held)
        }
    }
}

satellite.capsule change_both(satellite.container.list<satellite.variable.number> numbers, box b)
{
    numbers.append(4)
    b.put(99)
}

satellite.capsule satellite.main()
{
    satellite.container.list<satellite.variable.number> numbers = {1, 2, 3}
    box b
    satellite.variable.thread t = satellite.thread.new(change_both(numbers, b))
    t.start()
    t.join()
    satellite.console.display(numbers)
    satellite.console.display(b.get())
    satellite.return(satellite)
}
OUTPUT
{1, 2, 3}
99

Stopping a thread

.stop() asks a thread to stop at its next statement. It never cuts a statement in half. After a stop, .join() answers nothing.

stop.satlSATELLITE 004 Download
Stopping a loop that would never end
satellite.include(satellite)

satellite.capsule forever()
{
    satellite.variable.number spins = 0
    satellite.statement.while (spins >= 0)
    {
        spins = spins + 1
    }
}

satellite.capsule satellite.main()
{
    satellite.variable.thread spinner = satellite.thread.new(forever())
    spinner.start()
    spinner.stop()
    spinner.join()
    satellite.console.display(spinner)
    satellite.return(satellite)
}
OUTPUT
(thread forever, stopped)

Three more things worth knowing:

  • When main reaches satellite.return(satellite), every thread still running is asked to stop and is waited for. A thread started on the last line may never get to run, so join the threads whose work you need.
  • When several threads display at once, their lines may come in any order, but every line comes out whole.
  • A refusal on a thread is reported with its own line and caret, and .join() then stops the program with the same code.

Exercise 8

Count the numbers from 1 to 60,000 that are divisible by 3 or by 7. Split the work over three threads of 20,000 numbers each, add up what they answer, and check it against one plain call. (Remember that || is not built: use an else if.)

Answer 8

multiples.satlSATELLITE 004 Download
Three threads, one answer
satellite.include(satellite)

satellite.capsule count_in(satellite.variable.number from, satellite.variable.number to)
{
    satellite.variable.number found = 0
    satellite.statement.for(satellite.variable.number i = from; i <= to; i++)
    {
        satellite.statement.if (i % 3 == 0)
        {
            found = found + 1
        }
        satellite.statement.else satellite.statement.if (i % 7 == 0)
        {
            found = found + 1
        }
    }
    satellite.return(found)
}

satellite.capsule satellite.main()
{
    satellite.variable.thread first = satellite.thread.new(count_in(1, 20000))
    satellite.variable.thread second = satellite.thread.new(count_in(20001, 40000))
    satellite.variable.thread third = satellite.thread.new(count_in(40001, 60000))
    first.start()
    second.start()
    third.start()
    satellite.variable.number found = first.join() + second.join() + third.join()
    satellite.console.display(found)
    satellite.console.display(count_in(1, 60000))
    satellite.return(satellite)
}
OUTPUT
25714
25714

More on this lesson: THREADS.

LESSON 9: LOCKS

When several threads write to one object at the same time, their writes can land on top of each other. Four threads that each add 1 to a shared counter 5,000 times should reach 20,000. Without a lock they come up short, and by a different amount each run: two runs of the program below with its votes.lock() line taken out gave 11,719 and 11,520.

The fix is one line. Every object has a lock, and it starts off. .lock() turns it on, and from then on each statement that writes to the object holds the lock for that one statement.

tally.satlSATELLITE 004 Download
Four threads, one locked counter, an exact count
satellite.include(satellite)

satellite.spacesuit tally()
{
    satellite.protected
    {
        satellite.variable.number count = 0
    }

    satellite.public
    {
        satellite.capsule add_one()
        {
            count = count + 1
        }

        satellite.capsule total()
        {
            satellite.return(count)
        }
    }
}

satellite.capsule count_to(tally t, satellite.variable.number times)
{
    satellite.statement.for(satellite.variable.number i = 1; i <= times; i++)
    {
        t.add_one()
    }
}

satellite.capsule satellite.main()
{
    tally votes
    votes.lock()
    satellite.variable.thread a = satellite.thread.new(count_to(votes, 5000))
    satellite.variable.thread b = satellite.thread.new(count_to(votes, 5000))
    satellite.variable.thread c = satellite.thread.new(count_to(votes, 5000))
    satellite.variable.thread d = satellite.thread.new(count_to(votes, 5000))
    a.start()
    b.start()
    c.start()
    d.start()
    a.join()
    b.join()
    c.join()
    d.join()
    satellite.console.display(votes.total())
    satellite.return(satellite)
}
OUTPUT
20000

How the lock behaves:

  • Turning the lock on locks nothing by itself. A statement that writes the object holds the lock alone, for that one statement: assigning one of its fields, calling a method on one of its fields (such as items.append(x)), or calling one of its capsules.
  • A statement that only reads the object waits while a write is happening, and never waits for another read.
  • An if, while or for holds the lock for its condition only, never for its body.
  • A file has a lock too, and f.lock() turns it on. .unlock() turns either one off again.
  • Until .lock() is called, nothing is ever locked for an object, and threads share it freely.
  • If two threads end up waiting for each other, for example two objects locked in opposite orders, satl stops the line with S728 WAIT_NEVER_ENDS instead of freezing.

Exercise 9

Write a spacesuit guest_book with a list of names, a sign(name) capsule and a count() capsule. Start four threads that each sign it 250 times with a different name, wait for all four, and print the count.

Answer 9

guest_book.satlSATELLITE 004 Download
A locked list shared by four threads
satellite.include(satellite)

satellite.spacesuit guest_book()
{
    satellite.protected
    {
        satellite.container.list<satellite.variable.string> names = {}
    }

    satellite.public
    {
        satellite.capsule sign(satellite.variable.string name)
        {
            names.append(name)
        }

        satellite.capsule count()
        {
            satellite.return(names.size)
        }
    }
}

satellite.capsule visit(guest_book book, satellite.variable.string who)
{
    satellite.statement.for(satellite.variable.number i = 1; i <= 250; i++)
    {
        book.sign(who)
    }
}

satellite.capsule satellite.main()
{
    guest_book book
    book.lock()
    satellite.variable.thread a = satellite.thread.new(visit(book, "Ada"))
    satellite.variable.thread b = satellite.thread.new(visit(book, "Grace"))
    satellite.variable.thread c = satellite.thread.new(visit(book, "Alan"))
    satellite.variable.thread d = satellite.thread.new(visit(book, "Edsger"))
    a.start()
    b.start()
    c.start()
    d.start()
    a.wait()
    b.wait()
    c.wait()
    d.wait()
    satellite.console.display(book.count())
    satellite.return(satellite)
}
OUTPUT
1000

Leave the lock on for a list that several threads append to. Appending to an unlocked list from several threads at once can crash satl.

More on this lesson: LOCKS.

LESSON 10: READING AN ERROR REPORT

When something goes wrong, satl stops and prints a critical error report. It is worth learning to read one, because it tells you almost everything. Here is a program with a typing mistake:

typo.satlSATELLITE 004 Download
A misspelt name
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.number total = 10
    satellite.console.display("adding up")
    satellite.console.display(totl + 5)
    satellite.return(satellite)
}
OUTPUT
--------------------------------------------------------------------------------
SATELLITE CRITICAL ERROR REPORT
--------------------------------------------------------------------------------
S201: NAME_NOT_DECLARED
satl(check): in satellite.main, totl has no satellite.variable line declaring it

directory: typo.satl:7
syntax: satellite.console.display(totl + 5)
        /\

this name was used and no satellite.variable line ever declared it. A name has
to be given a type before it can hold anything.
machine code 25 name_not_declared -- satl exits with this.

--------------------------------------------------------------------------------

Read it from the top:

  1. S201: NAME_NOT_DECLARED is the S-code and its name. The number says how bad it is: S0xx is a warning and the run carries on, and the numbers climb from there.
  2. satl(check): in satellite.main, ... says what went wrong, in a sentence. check means satl found it while checking the program, before anything ran, which is why adding up was never printed. A report that says satl(run) was found while the program was running, after the lines before it had done their work.
  3. directory: typo.satl:7 is the file and the line number.
  4. syntax: quotes that line, and the caret /\ points into it.
  5. The paragraph after it explains the kind of mistake in general.
  6. machine code 25 is the exit status. satl exits 0 when a program runs to its end, and with the machine code when it stops, so a script can test it. The report goes to standard error, like the title block, so 2> /dev/null hides both:
BASH Download
satl typo.satl 2> /dev/null
echo $?
TEXT Download
25

Here is a mistake found while running. The program printed start before it reached line 7:

spaces.satlSATELLITE 004 Download
An operator without spaces
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.number n = 4
    satellite.console.display("start")
    satellite.console.display(n+3)
    satellite.return(satellite)
}
OUTPUT
--------------------------------------------------------------------------------
SATELLITE CRITICAL ERROR REPORT
--------------------------------------------------------------------------------
S110: LINE_NOT_UNDERSTOOD
satl(run): every math operation is written with a space on both sides of the
sign, and this + has none

directory: spaces.satl:7
syntax: satellite.console.display(n+3)
        /\

satellite has no scenario for this line -- it is spelled in a shape the language
does not have a meaning for.
machine code 13 satl_line_not_understood -- satl exits with this.

--------------------------------------------------------------------------------

A few refusals are still reported on a single line that starts with [satellite], with the machine code in brackets at its end. A value of the wrong type handed to a capsule is one of them:

wrong_type.satlSATELLITE 004 Download
A string handed to a number parameter
satellite.include(satellite)

satellite.capsule double(satellite.variable.number n)
{
    satellite.return(n * 2)
}

satellite.capsule satellite.main()
{
    satellite.console.display(double(21))
    satellite.console.display(double("21"))
    satellite.return(satellite)
}
OUTPUT
[satellite] satl(run): double's n was declared satellite.variable.number, and it holds a string (machine_code: 27 types_do_not_meet)

The first call printed 42; the second stopped the program with machine code 27.

Exercise 10

This program should print the average of three test scores. Run it, read the report, and fix it.

average.satlSATELLITE 004 Download
The broken version
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.number total = 72 + 95 + 88
    satellite.console.dispaly("average: " + total / 3)
    satellite.return(satellite)
}
OUTPUT
--------------------------------------------------------------------------------
SATELLITE CRITICAL ERROR REPORT
--------------------------------------------------------------------------------
S110: LINE_NOT_UNDERSTOOD
satl(check): in satellite.main, satellite.console has no word named dispaly --
did you mean satellite.console.display?

directory: average.satl:6
syntax: satellite.console.dispaly("average: " + total / 3)
        /\

satellite has no scenario for this line -- it is spelled in a shape the language
does not have a meaning for.
machine code 13 satl_line_not_understood -- satl exits with this.

--------------------------------------------------------------------------------

Answer 10

The report says satellite.console has no word named dispaly, and asks whether you meant satellite.console.display. You did: the two letters are the wrong way round. Nothing ran, because satl checks every word before it starts. Spell it right:

average.satlSATELLITE 004 Download
The fixed version
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.number total = 72 + 95 + 88
    satellite.console.display("average: " + total / 3)
    satellite.return(satellite)
}
OUTPUT
average: 85

More on this lesson: READING AN ERROR REPORT.

WHERE TO GO NEXT

You have now used every part of the language that most programs need. From here: