The Satellite Corporation Broadcasting Another World!

COLOR ON THE CONSOLE

THE SATELLITE CORPORATION

DATE:
ID: 12/42

USERNAME: Claude Code
CATEGORY: SATELLITE 004

Plain text gets the job done, but colour makes a program easier to read at a glance: green for “it worked”, orange for “careful”. Satellite gives you three ways to colour what your program shows, plus a few words about the screen itself.

A colour is written as six hex digits after an x, two each for red, green and blue: xFF0000 is red, x00FF00 is green, xFF8800 is orange. You can also keep one in a satellite.variable.color (or satellite.variable.colour, both spellings work) and use its name. COLORS explains colours in full.

The output boxes on this page show plain text. When satl writes into a pipe or a file, as it does when these examples are checked, the colours are left out and only the words go through. At a terminal, you see the colours.

Styling one line

satellite.console.display takes named options after the value, each written name=value:

  • foreground= the colour of the text
  • background= the colour behind it
  • bold=satellite.bool.true and italic=satellite.bool.true

The style lasts for that one line, and the next line is plain again.

styled_lines.satlSATELLITE 004 Download
One line in orange and bold, one in a colour held by name
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.console.display("careful", foreground=xFF8800, bold=satellite.bool.true)
    satellite.variable.color sky = x87CEEB
    satellite.console.display("a clear day", foreground=x000000, background=sky)
    satellite.console.display("a thought", italic=satellite.bool.true)
    satellite.console.display("and this line is plain")
    satellite.return(satellite)
}
OUTPUT
careful
a clear day
a thought
and this line is plain

A colour written straight into the line must be six hex digits. foreground=xFFF or foreground="red" is refused with S301 before a single line of the program runs, so a typo never waits until halfway through to show itself.

Colouring one word

Sometimes only one word should stand out. A string has its own .foreground(c) and .background(c), which answer the same string in that colour. Join it into a line like any other string:

status.satlSATELLITE 004 Download
Only the OK is green
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.string ok = "OK".foreground(x00FF00)
    satellite.console.display("status: " + ok)
    satellite.variable.color warning = xFF8800
    satellite.console.display("disk: " + "nearly full".foreground(warning))
    satellite.console.display(ok == "OK")
    satellite.return(satellite)
}
OUTPUT
status: OK
disk: nearly full
false

Why is the last line false? Because the colour is really part of the string. It is stored as the terminal’s own colour codes around the letters, so ok is not the same two letters as "OK". Display leaves those codes out when it writes into a pipe or a file, or when the NO_COLOR setting is on. A string you save into a file yourself keeps them, so colour your words at the moment you show them.

Colouring every line after

satellite.console.foreground(c) sets the colour for every line display writes from then on, and satellite.console.background(c) does the same behind the text. Written with empty brackets, satellite.console.foreground() puts the terminal’s own colour back. A line’s own foreground= still wins while it is set.

green_console.satlSATELLITE 004 Download
Every line green until you say stop
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.console.foreground(x33FF33)
    satellite.console.display("every line is green now")
    satellite.console.display("this one too")
    satellite.console.display("except this one", foreground=xFF0000)
    satellite.console.foreground()
    satellite.console.display("and back to normal")
    satellite.return(satellite)
}
OUTPUT
every line is green now
this one too
except this one
and back to normal

This colours your program’s lines. To change the colours of the whole terminal window, empty rows included, see THE TERMINAL.

The screen: width, height, clear and home

satellite.console.width and satellite.console.height, with no brackets, answer how many characters fit across the terminal and how many lines fit down it. They are asked fresh every time, so they follow the window when someone resizes it. When there is no terminal, they answer 80 and 24, which is what the example below shows because it was checked without one:

screen_size.satlSATELLITE 004 Download
How big is the screen?
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.number across = satellite.console.width
    satellite.variable.number down = satellite.console.height
    satellite.console.display("the screen is " + across.to_string() + " characters across")
    satellite.console.display("and " + down.to_string() + " lines down")
    satellite.return(satellite)
}
OUTPUT
the screen is 80 characters across
and 24 lines down

Two more words move things on the screen:

  • satellite.console.clear() empties the screen and puts the cursor in the top-left corner.
  • satellite.console.home() only moves the cursor to the top-left corner and leaves the screen as it is, so the next thing you display is drawn over the last.

Both write the terminal’s own codes for this, even when the output goes into a file.

Where next

Every option and colour word is in the FULL REFERENCE. For more on display itself, read DISPLAYING TEXT, and for colour values, COLORS.

THE TERMINAL

THE SATELLITE CORPORATION

DATE:
ID: 11/42

USERNAME: Claude Code
CATEGORY: SATELLITE 004

When you run a satellite program, it runs inside a terminal: the window around the text. That might be the terminal program you use every day, or the console window satl opens by itself. In satellite, that window is satellite.terminal.

It is a different thing from satellite.console. The console is the stream of lines your program writes and reads. The terminal is the window they appear in. So satellite.console.foreground colours your program’s lines, and satellite.terminal.foreground colours the terminal itself.

Changing the terminal’s colours

satellite.terminal.foreground(c) and satellite.terminal.background(c) change the terminal’s own colours. That means everything drawn in its default colour, and the empty rows too, not only the lines written after them. A colour is six hex digits, like x101010, or a satellite.variable.color held by name.

night_mode.satlSATELLITE 004 Download
Green text on near-black, for the whole window
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.terminal.background(x101010)
    satellite.terminal.foreground(x33FF33)
    satellite.console.display("green on near-black, the whole terminal")
    satellite.console.display("and your own colours come back when this ends")
    satellite.return(satellite)
}
OUTPUT
green on near-black, the whole terminal
and your own colours come back when this ends

The output box shows only the words. The colours are a change to the terminal, not to the text, so they never appear in what your program prints.

Putting the colours back

Write the same words with empty brackets, satellite.terminal.foreground() and satellite.terminal.background(), and the terminal’s own colours come back straight away. Here the colours live in names, so they are easy to change in one place:

reading_lamp.satlSATELLITE 004 Download
Set the colours from names, then put them back
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.color paper = xFFF8E7
    satellite.variable.color ink = x3B2F2F
    satellite.terminal.background(paper)
    satellite.terminal.foreground(ink)
    satellite.console.display("dark ink on warm paper")
    satellite.terminal.foreground()
    satellite.terminal.background()
    satellite.console.display("your own colours again")
    satellite.return(satellite)
}
OUTPUT
dark ink on warm paper
your own colours again

You do not have to remember to do this. satl puts back whatever it changed when the program ends, however it ends: when it finishes, when it stops on an error, when you press Ctrl-C, or when it crashes. A program never leaves your shell orange.

One thing to know: “back” means the colours from your terminal’s own settings. If your shell had already changed the terminal’s colours in the same way, satl does not remember that change, and you get the terminal’s settings instead.

When nothing changes

The terminal is only touched when there is one to touch:

  • When your program’s output goes into a pipe or a file, satl does not change any colours. That is why the examples on this page could be checked without a terminal at all.
  • When the NO_COLOR setting is on, satl leaves the terminal’s colours alone.

A colour that is not a colour

A colour must be six hex digits or a colour held by name. Text is refused before the program starts, so nothing is half changed:

orange_words.satlSATELLITE 004 Download
A colour's name in words is not a colour
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.terminal.foreground("orange")
    satellite.return(satellite)
}
OUTPUT
--------------------------------------------------------------------------------
SATELLITE CRITICAL ERROR REPORT
--------------------------------------------------------------------------------
S301: TYPES_DO_NOT_MEET
satl(check): in satellite.main, satellite.terminal.foreground takes a colour --
six hex digits like xFF8800, or a satellite.variable.color -- and was given text

directory: orange_words.satl:5
syntax: satellite.terminal.foreground("orange")
        /\

this operator has no scenario for the two kinds it was given. Nothing was
guessed at, because a guess here is an answer that is wrong and does not say so.
machine code 27 types_do_not_meet -- satl exits with this.

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

satl’s own console

If you start a program without a terminal, for example from a desktop launcher, satl opens a console window of its own to run it in. You can also ask for one:

BASH Download
satl --console night_mode.satl

That console is a terminal like any other, so satellite.terminal colours it the same way. Your program’s input and output are that window, and when the program finishes, the window closes. If a program stops on an error, the window stays open and says so, so you can read the report, and closes when you press a key. Setting SATL_NO_WINDOW stops satl from opening a console by itself.

Where next

Every terminal and console word is in the FULL REFERENCE. To colour single lines and words instead of the whole window, read COLOR ON THE CONSOLE, and for everything the satl command can do, THE SATL COMMAND.

FILES

THE SATELLITE CORPORATION

TITLE: FILES
DATE:
ID: 10/42

USERNAME: Claude Code
CATEGORY: SATELLITE 004

Programs forget everything when they end, unless they write it down. Satellite’s files are built on one simple idea: a text file is a list of lines that lives on the disk. Line 1 is the first line, each line is a string, and most of what you can do to a list you can do to a file, spelled the same way.

A file is held in a satellite.variable.file. You get one from satellite.file.new or satellite.file.open, and after that you work through its name.

Making a file and reading it back

  • satellite.file.new("name.txt") makes a new, empty text file and opens it. It never replaces a file that is already there.
  • f.append("text") adds a line at the end.
  • f[n] reads line n, counting from 1. f.first and f.last read the first and last lines, and f.size says how many lines there are.
  • f.close() saves and closes the file, and answers true when the changes reached the disk.
shopping.satlSATELLITE 004 Download
Write three lines, then read them back
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.file shopping = satellite.file.new("shopping.txt")
    shopping.append("bread")
    shopping.append("milk")
    shopping.append("eggs")
    satellite.console.display(shopping.size)
    satellite.console.display(shopping[1])
    satellite.console.display(shopping.last)
    satellite.console.display(shopping.close())
    satellite.return(satellite)
}
OUTPUT
3
bread
eggs
true

After this runs, shopping.txt sits beside the program with bread, milk and eggs on three lines. A path without a / at the front is found in the folder of the .satl file that uses it, wherever you ran satl from.

Opening a file and changing it

satellite.file.open("name.txt") opens a text file that is already there. These change its lines:

  • f.insert(n, "text") puts a new line in as line n, and everything from there down moves down one.
  • f.replace(n, "text") makes line n say something else.
  • f.replace("old", "new") changes the first place old appears in the file into new. A number first means a line, and text first means text.
  • f.remove_at(n), f.remove("a line"), f.remove_first(), f.remove_last(), f.truncate(n) and f.clear() take lines out.

And these find things without changing anything:

  • f.index_of("a line") answers the number of the first line that is exactly that text, and f.search("text") the first line that contains it anywhere. When nothing matches, both answer 0, which is never a line.
  • f.contains("a line") answers true or false.
  • f.read_all answers the whole file as one string, line endings included.

To walk every line, count from 1 up to f.size:

shopping_edit.satlSATELLITE 004 Download
Make a file, open it again, change it and walk it
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.file shopping = satellite.file.new("shopping.txt")
    shopping.append("bread")
    shopping.append("milk")
    shopping.append("eggs")
    shopping.close()

    satellite.variable.file again = satellite.file.open("shopping.txt")
    again.insert(2, "butter")
    again.replace(1, "brown bread")
    again.replace("milk", "oat milk")
    satellite.console.display(again.search("milk"))
    satellite.console.display(again.index_of("cheese"))
    satellite.console.display(again.contains("butter"))
    satellite.statement.for(satellite.variable.number i = 1; i <= again.size; i + 1)
    {
        satellite.console.display(i.to_string() + ". " + again[i])
    }
    satellite.return(satellite)
}
OUTPUT
3
0
true
1. brown bread
2. butter
3. oat milk
4. eggs

Notice there is no again.close() at the end. Satellite saves and closes every file that is still open when the program ends, so forgetting to close loses nothing. You can also save part way through with f.save().

A save is whole or not at all. Satellite writes the new version beside the old one and swaps it in, so if the program is stopped in the middle of a save, the file holds either the old lines or the new ones, never a mixture.

When a file will not open

Opening or making a file never stops your program. Instead, the file you get back is not open, and you can ask it:

  • f.ok answers whether it is open.
  • f.error says why not, in words.
  • satellite.file.exists("name.txt") answers whether a file is there at all.
careful_opening.satlSATELLITE 004 Download
open never makes a file, and new never replaces one
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.file missing = satellite.file.open("no_such_file.txt")
    satellite.console.display(missing.ok)
    satellite.console.display(satellite.file.exists("no_such_file.txt"))

    satellite.variable.file diary = satellite.file.new("diary.txt")
    diary.append("day one")
    diary.close()
    satellite.variable.file second = satellite.file.new("diary.txt")
    satellite.console.display(second.ok)

    satellite.variable.file reopened = satellite.file.open("diary.txt")
    satellite.console.display(reopened[1])
    satellite.return(satellite)
}
OUTPUT
false
false
false
day one

open never makes a file, so a misspelled name cannot quietly become an empty one. new never replaces one, so your diary survives: the second new came back not open, and the first day is still there.

What is refused

A few things stop the program, because any answer to them would be made up:

  • Reading a line that is not there, like line 2 of a one-line file, is S501 LINE_PAST_THE_END.
  • Asking about the lines of a file that is not open, like missing.size, is S503 FILE_NOT_OPEN. Check f.ok first.
  • A method a file does not have, such as f.write("text"), or the wrong number of arguments, such as f.replace(1), is refused before the program starts. To add a line, use append or insert.
past_the_end.satlSATELLITE 004 Download
Line 2 of a one-line file
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.file notes = satellite.file.new("notes.txt")
    notes.append("the only line")
    satellite.console.display(notes[1])
    satellite.console.display(notes[2])
    satellite.return(satellite)
}
OUTPUT
--------------------------------------------------------------------------------
SATELLITE CRITICAL ERROR REPORT
--------------------------------------------------------------------------------
S501: LINE_PAST_THE_END
satl(run): notes[2]: there is no such line -- the file has 1 line, counting from
1

directory: past_the_end.satl:8
syntax: satellite.console.display(notes[2])
        /\

a line was read by its number and the file has no line with that number. Lines
count from 1, and the last one is the file's size.
machine code 47 line_past_the_end -- satl exits with this.

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

The first line was shown before the report. The file itself is safe: notes.txt was still saved with its one line.

Only text files are built today. Asking for a binary file, satellite.file.new("data.bin", "binary"), gives a file that is not open.

Where next

Every file word is in the FULL REFERENCE. Files behave like LISTS, and two threads can share one safely with a lock: see LOCKS: SHARING AN OBJECT BETWEEN THREADS.

WINDOWS

THE SATELLITE CORPORATION

TITLE: WINDOWS
DATE:
ID: 9/42

USERNAME: Claude Code
CATEGORY: SATELLITE 004

Satellite can open real windows on your desktop, with buttons, text boxes, sliders, menus and drawings in them. Everything you need is under one word, satellite.window, and everything it makes is held in one type, satellite.variable.window.

Every example on this page opens a window, so none has an output box. Save one and run it on your own desktop with satl name.satl.

Your first window

Three steps make every window:

  1. Make the window with satellite.window.new(title, width, height).
  2. Make a piece, such as satellite.window.button("text").
  3. Put the piece in the window with my_window.append(piece, across, down). The position is where the piece’s centre goes, so 400, 300 is the middle of an 800 by 600 window.

A piece talks back by running a capsule you name. .pressed(when_pressed) on a button means “when someone presses this, run the capsule when_pressed“. You write the capsule’s name bare, with no quotes, and satl checks that capsule exists before the program starts.

first_window.satlSATELLITE 004 Download
A window with one button that closes it
satellite.include(satellite)

satellite.capsule when_pressed(satellite.variable.window the_piece, satellite.variable.window its_window)
{
    satellite.console.display("the button was pressed")
    its_window.close()
}

satellite.capsule satellite.main()
{
    satellite.variable.window my_window = satellite.window.new("window_title", 800, 600)

    satellite.variable.window my_button = satellite.window.button("text")
    my_button.pressed(when_pressed)

    my_window.append(my_button, 400, 300)

    satellite.console.display(my_window)

    satellite.return(satellite)
}

The program’s own lines run straight away, so (window "window_title") appears in your terminal at once. Then satl waits: a program does not end while one of its windows is open. Press the button and the capsule runs, writes its line and closes the window, and the program ends. Closing the window yourself ends it too.

A capsule named by .pressed may take nothing, the piece that was pressed, or the piece and the window it is in. There are no global variables in satellite, so the window handed to the capsule is how it reaches the window to close it.

Pieces, and reading what people type

These make pieces, each a satellite.variable.window:

  • button(text), label(text), text_box(text), text_area(text), checkbox(text), switch()
  • slider(least, most), number_box(least, most), progress(), choice(list), one_of(list)
  • row(), column() and grid() to arrange other pieces, and scroll(), frame(title), split() and tabs() to hold them
  • picture(path), canvas(width, height) and menu(title)

A row or a column takes .append(piece) and lines its pieces up. Most pieces can be read and set with the same word: .text reads the text, and .text("hi") sets it. .value, .on, .chosen and .title work the same way. .changed(a_capsule) runs a capsule whenever someone changes a piece.

name_form.satlSATELLITE 004 Download
A text box that reports each change, and a done button
satellite.include(satellite)

satellite.capsule when_typed(satellite.variable.window the_box)
{
    satellite.console.display(the_box.text)
}

satellite.capsule when_done(satellite.variable.window the_button, satellite.variable.window its_window)
{
    its_window.close()
}

satellite.capsule satellite.main()
{
    satellite.variable.window my_window = satellite.window.new("a form", 400, 300)
    satellite.variable.window a_column = satellite.window.column()
    satellite.variable.window your_name = satellite.window.text_box("")
    your_name.changed(when_typed)
    satellite.variable.window done = satellite.window.button("done")
    done.pressed(when_done)
    a_column.append(your_name)
    a_column.append(done)
    my_window.append(a_column, 200, 150)
    satellite.return(satellite)
}

Drawing on a canvas

A canvas is a piece you draw on. Its pen draws in the canvas’s .colour(...), and positions count from its top-left corner:

  • .line(x1, y1, x2, y2) from one point to another
  • .box(x, y, width, height) from its top-left corner
  • .circle(x, y, radius) and .arc(x, y, radius, from, to), with the angles in degrees
  • .write(x, y, "words"), in the font from .font("IBM Plex Mono", 18)
  • .clear() wipes it, and .save("picture.png") saves what is drawn as a picture
drawing.satlSATELLITE 004 Download
A line, a box, a circle and some words
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.window my_window = satellite.window.new("a drawing", 400, 300)
    satellite.variable.window c = satellite.window.canvas(400, 300)
    c.colour("#40c8ff")
    c.line(0, 0, 399, 299)
    c.box(20, 20, 80, 50)
    c.circle(100, 200, 30)
    c.font("IBM Plex Mono", 18)
    c.write(150, 40, "hello from satellite")
    my_window.append(c, 200, 150)
    satellite.return(satellite)
}

Things a window can do

A window has its own words too:

  • .close(), .focus(), .resize(1024, 768) and .fullscreen(1)
  • .message("saved") shows a message, and .ask(a_capsule, "sure?") asks a question and runs the capsule with the answer in its_window.answer
  • .every(a_capsule, 1000) runs a capsule every 1000 milliseconds, for as long as the window is open
  • .closed(a_capsule) runs a capsule when the window is closed, and .key(a_capsule) when a key is pressed
ticking.satlSATELLITE 004 Download
A clock that ticks once a second until you press stop
satellite.include(satellite)

satellite.capsule tick()
{
    satellite.console.display("tick")
}

satellite.capsule stop(satellite.variable.window the_button, satellite.variable.window its_window)
{
    its_window.close()
}

satellite.capsule satellite.main()
{
    satellite.variable.window w = satellite.window.new("a clock", 300, 200)
    satellite.variable.window done = satellite.window.button("stop")
    done.pressed(stop)
    w.append(done, 150, 100)
    w.every(tick, 1000)
    satellite.return(satellite)
}

What satl refuses, and when

  • Before anything runs: a wrong number of arguments, a method no piece has, and a capsule nobody wrote. A method this particular piece lacks, like .resize on a button, is refused when its line runs.
  • Write 800, 600, never 800×600. Satellite reads x600 as a hex number.
  • No screen, no window. On a machine with no display, such as a server reached over ssh, the first window word stops with S730 NO_DISPLAY and satl exits with 50. That is the machine, not your program: the same file opens its window on a desktop.
  • Windows stay on the main thread. A window word used on a thread of your own is refused with S727.

One more word makes a window: satellite.console.new(title, width, height) makes a console of your own, a window whose whole inside is a terminal, with c.display("words") to write into it and c.typed(a_capsule) to hear each line a person types.

Where next

Every window word, piece and method is listed in the FULL REFERENCE. A window’s capsules are ordinary CAPSULES, and the help system has both window topics: see THE HELP SYSTEM.

THREADS

THE SATELLITE CORPORATION

TITLE: THREADS
DATE:
ID: 8/42

USERNAME: Claude Code
CATEGORY: SATELLITE 004

A program normally does one thing at a time, line after line. A thread lets it do several: a capsule runs on a thread of its own, beside the rest of your program, and your program collects its answer when it is ready. On a machine with several cores, threads really do run at the same moment.

Making, starting and joining a thread

A thread is held in a satellite.variable.thread, and it takes three steps:

  1. satellite.thread.new(my_capsule(arguments)) makes the thread. The arguments are worked out now, but the capsule does not begin yet.
  2. t.start() sets the capsule running on its own thread and carries straight on to your next line.
  3. t.join() waits until the capsule is done, and answers what it handed back. t.wait() is a second name for the same thing.

What goes inside satellite.thread.new must be a call of one of your own capsules: satellite.thread.new(5) is refused before the program starts, with S721. Displaying a thread shows which capsule it runs and how far it has got.

square.satlSATELLITE 004 Download
One thread, from start to answer
satellite.include(satellite)

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

satellite.capsule satellite.main()
{
    satellite.variable.thread t = satellite.thread.new(square(12))
    satellite.console.display(t)
    t.start()
    satellite.variable.number answer = t.join()
    satellite.console.display(answer)
    satellite.console.display(t)
    satellite.return(satellite)
}
OUTPUT
(thread square, not started)
144
(thread square, finished)

A thread runs once. Joining it again answers the same value again, with a one-line notice that it was already joined, but starting it a second time is refused with S722. To run the capsule again, make a new thread.

Sharing out the work

Threads earn their keep when a job splits into parts that do not depend on each other. Here four threads each add up a quarter of the numbers from 1 to 1000, and the program adds their four answers together:

four_sums.satlSATELLITE 004 Download
Four threads, one sum
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 + 1)
    {
        total = total + i
    }
    satellite.return(total)
}

satellite.capsule satellite.main()
{
    satellite.variable.thread first = satellite.thread.new(add_up(1, 250))
    satellite.variable.thread second = satellite.thread.new(add_up(251, 500))
    satellite.variable.thread third = satellite.thread.new(add_up(501, 750))
    satellite.variable.thread fourth = satellite.thread.new(add_up(751, 1000))
    first.start()
    second.start()
    third.start()
    fourth.start()
    satellite.variable.number sum = first.join() + second.join() + third.wait() + fourth.wait()
    satellite.console.display(sum)
    satellite.console.display(add_up(1, 1000))
    satellite.return(satellite)
}
OUTPUT
500500
500500

All four threads are started before any is joined, so they run at the same time. The last line runs add_up the ordinary way, on one thread, to check the answer.

The four answers can be added in any order, which is what makes this work. When threads display lines of their own, every line comes out whole, but which thread’s line comes first is up to the machine. Join a thread before you display what it found, and your output stays in order.

Stopping a thread

t.stop() asks a thread to stop at its next statement. It never cuts a statement off in the middle. Join it afterwards to wait until it has really stopped:

spinner.satlSATELLITE 004 Download
Stop a thread that would never end by itself
satellite.include(satellite)

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

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

You do not have to stop every thread yourself. When your program reaches satellite.return(satellite), satellite asks every thread still running to stop, and waits for it.

When a thread goes wrong

Joining a thread that was never started is refused, because there would be nothing to wait for:

too_soon.satlSATELLITE 004 Download
join before start
satellite.include(satellite)

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

satellite.capsule satellite.main()
{
    satellite.variable.thread t = satellite.thread.new(square(3))
    satellite.console.display(t.join())
    satellite.return(satellite)
}
OUTPUT
--------------------------------------------------------------------------------
SATELLITE CRITICAL ERROR REPORT
--------------------------------------------------------------------------------
S723: JOIN_BEFORE_START
satl(run): t.join() -- square was never started, so there is nothing to wait
for; t.start() comes first

directory: too_soon.satl:11
syntax: satellite.console.display(t.join())
        /\

join() and wait() wait for a thread that is running, and this one has not been
started -- start() comes first.
machine code 58 thread_not_started -- satl exits with this.

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

If a capsule fails on its thread, for example by dividing by zero, the report shows the line on the thread where it happened. Joining that thread then stops your program with the same code. A thread that fails and is never joined still fails the whole run.

What threads share

Numbers, strings and lists are values: each thread gets its own copy, so one thread changing its copy never surprises another. An object made from a spacesuit, or a file, is different. Hand one to a thread and both sides hold the same object. That is useful, and it needs care when two threads change it at once, which is what LOCKS are for. Windows stay with the main thread: a window word used on a thread is refused with S727.

Where next

Every thread word is in the FULL REFERENCE. Next, read LOCKS: SHARING AN OBJECT BETWEEN THREADS, and for the objects themselves, SPACESUITS.

LOCKS: SHARING AN OBJECT BETWEEN THREADS

THE SATELLITE CORPORATION

DATE:
ID: 7/42

USERNAME: Claude Code
CATEGORY: SATELLITE 004

When you hand an object to several threads, they all hold the same object. That is exactly what you want when they work on one shared result. But a line like total = total + 1 is really three steps: read total, add one, write it back. If two threads read the same old value at the same moment, both write back the same new one, and one of the additions is lost.

Satellite’s answer is a lock that you turn on yourself.

Turning the lock on

Every object made from a spacesuit, and every file, carries a lock, and it starts off. While it is off, nothing is ever locked and threads share the object freely, at full speed.

obj.lock() turns the lock on. That does not lock anything by itself. Instead, from then on, each statement inside the object’s own capsules takes the lock for just that one statement:

  • A statement that writes the object holds the lock alone. Writing means assigning one of its fields (total = total + 1), calling a method on a field (items.append(x)), or calling a capsule.
  • A statement that only reads the object shares the lock with other readers. It waits only while a write is happening, or while a writer is waiting its turn, so reads can never starve a write.
  • An if, while or for holds the lock for its condition only, never for its whole body.

obj.unlock() turns the lock off again.

Here four threads each add 1 to one shared counter a thousand times. With the lock on, every addition counts:

shared_counter.satlSATELLITE 004 Download
Four threads, one locked counter, nothing lost
satellite.include(satellite)

satellite.spacesuit counter()
{
    satellite.protected
    {
        satellite.variable.number total = 0
    }
    satellite.public
    {
        satellite.capsule call_add()
        {
            total = total + 1
        }
        satellite.capsule call_total()
        {
            satellite.return(total)
        }
    }
}

satellite.capsule count_to_a_thousand(counter c)
{
    satellite.statement.for(satellite.variable.number i = 1; i <= 1000; i + 1)
    {
        c.call_add()
    }
}

satellite.capsule satellite.main()
{
    counter shared()
    shared.lock()
    satellite.variable.thread a = satellite.thread.new(count_to_a_thousand(shared))
    satellite.variable.thread b = satellite.thread.new(count_to_a_thousand(shared))
    satellite.variable.thread c = satellite.thread.new(count_to_a_thousand(shared))
    satellite.variable.thread d = satellite.thread.new(count_to_a_thousand(shared))
    a.start()
    b.start()
    c.start()
    d.start()
    a.join()
    b.join()
    c.join()
    d.join()
    shared.unlock()
    satellite.console.display(shared.call_total())
    satellite.return(satellite)
}
OUTPUT
4000

Take the shared.lock() line out and the same program comes up short, by a different amount each run: around 2,500 is typical. With a list field instead of a number, two threads changing it unlocked can even crash satl. If threads share an object and more than one of them changes it, lock it.

Locking a file

A file’s lock works the same way. With it on, each method called on the file holds the lock for that call, and reading a line with f[n] is a read. Two threads can then append to one file, and no line is lost or torn:

shared_log.satlSATELLITE 004 Download
Two threads appending to one locked file
satellite.include(satellite)

satellite.capsule write_lines(satellite.variable.file log, satellite.variable.string who)
{
    satellite.statement.for(satellite.variable.number i = 1; i <= 50; i + 1)
    {
        log.append(who + " " + i.to_string())
    }
}

satellite.capsule satellite.main()
{
    satellite.variable.file log = satellite.file.new("log.txt")
    log.lock()
    satellite.variable.thread left = satellite.thread.new(write_lines(log, "left"))
    satellite.variable.thread right = satellite.thread.new(write_lines(log, "right"))
    left.start()
    right.start()
    left.join()
    right.join()
    satellite.console.display(log.size)
    satellite.console.display(log.contains("left 50"))
    satellite.console.display(log.contains("right 50"))
    satellite.return(satellite)
}
OUTPUT
100
true
true

A join lets go while it waits

A statement holding a lock should not sit on it while it waits for something slow, and waiting for a thread is the slowest thing there is. So join() and wait() give back every lock their line holds, wait for the thread, then take the locks back before the line finishes. That lets a capsule of a locked object start a helper thread on the same object and wait for its answer:

helper.satlSATELLITE 004 Download
total = helper.join() on a locked object
satellite.include(satellite)

satellite.spacesuit counter()
{
    satellite.protected
    {
        satellite.variable.number total = 0
    }
    satellite.public
    {
        satellite.capsule call_next()
        {
            satellite.return(total + 1)
        }
        satellite.capsule call_step()
        {
            satellite.variable.thread helper = satellite.thread.new(call_next())
            helper.start()
            total = helper.join()
        }
        satellite.capsule call_total()
        {
            satellite.return(total)
        }
    }
}

satellite.capsule satellite.main()
{
    counter c()
    c.lock()
    c.call_step()
    c.call_step()
    c.call_step()
    satellite.console.display(c.call_total())
    satellite.return(satellite)
}
OUTPUT
3

The helper reads total while call_step is waiting for it, which works only because the join let go. The one thing to keep in mind: in a line like total = total + w.join(), another thread may change total between the moment the line reads it and the moment it writes it, because the line does not hold the lock during the join.

A circle that would never end

Some waits can never finish. Here two jars are both locked. One thread fills the left jar by asking the right jar for a scoop, while the other fills the right jar from the left. Each line that changes a jar holds that jar’s lock while it asks the other jar, so each thread ends up holding one lock and waiting for the other’s. Neither can ever go on.

Satellite does not freeze. Before any thread goes to sleep, it follows the chain of who is waiting for whom, and if the chain comes back to itself, the line stops with S728 WAIT_NEVER_ENDS:

jars.satlSATELLITE 004 Download
Two locked jars, each waiting for the other
satellite.include(satellite)

satellite.spacesuit jar()
{
    satellite.protected
    {
        satellite.variable.number beans = 0
        satellite.variable.number scoop = 1
    }
    satellite.public
    {
        satellite.capsule call_fill_from(jar other)
        {
            satellite.statement.for(satellite.variable.number i = 1; i <= 2000; i + 1)
            {
                beans = beans + other.call_scoop()
            }
        }
        satellite.capsule call_scoop()
        {
            satellite.return(scoop)
        }
    }
}

satellite.capsule satellite.main()
{
    jar left()
    jar right()
    left.lock()
    right.lock()
    satellite.variable.thread x = satellite.thread.new(left.call_fill_from(right))
    satellite.variable.thread y = satellite.thread.new(right.call_fill_from(left))
    x.start()
    y.start()
    x.join()
    y.join()
    satellite.console.display("never reached")
    satellite.return(satellite)
}
OUTPUT
--------------------------------------------------------------------------------
SATELLITE CRITICAL ERROR REPORT
--------------------------------------------------------------------------------
S728: WAIT_NEVER_ENDS
satl(run): this line needs its object's lock, and a thread holding it is waiting
-- through locks and joins -- for this one

directory: jars.satl:21
syntax: satellite.return(scoop)
        /\

this line would wait forever: the object's lock it needs is held by a thread
that is itself waiting -- through locks and joins -- for this one. Two threads
each waiting for the other never end, so satellite stops this line instead of
the program freezing (003's S1407 and S1408). A line that holds an object's lock
-- writing it, or reading it -- should not wait for a thread that needs the same
object's lock.
machine code 63 wait_never_ends -- satl exits with this.

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

The cure is to stop one locked object’s line from waiting on another. Give the jar a plain call_add(more), and do the moving in an ordinary capsule outside the spacesuit: into.call_add(from.call_scoop()). Each jar’s lock is then held only by a line of its own that asks nothing of the other jar, so no circle can form, and the two threads finish with all 4,000 beans.

Where next

Every thread and lock word is in the FULL REFERENCE. Start with THREADS if you have not already, and see SPACESUITS for the objects being shared.

READING AN ERROR REPORT

THE SATELLITE CORPORATION

DATE:
ID: 6/42

USERNAME: Claude Code
CATEGORY: SATELLITE 004

Everybody’s programs go wrong, and the first programs go wrong the most. When satellite cannot run something, it never guesses and never goes quiet. It stops and prints a SATELLITE CRITICAL ERROR REPORT that says what happened, where, and usually what to do about it. Learning to read one takes a minute and saves hours.

The parts of a report

Here is a small mistake: the program uses pears but never declared it.

fruit.satlSATELLITE 004 Download
pears was never declared
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.number apples = 3
    satellite.console.display(apples + pears)
    satellite.return(satellite)
}
OUTPUT
--------------------------------------------------------------------------------
SATELLITE CRITICAL ERROR REPORT
--------------------------------------------------------------------------------
S201: NAME_NOT_DECLARED
satl(check): in satellite.main, pears has no satellite.variable line declaring
it

directory: fruit.satl:6
syntax: satellite.console.display(apples + pears)
        /\

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. The S-code and its name, S201: NAME_NOT_DECLARED. The code is short to search for, and the name says it in words.
  2. What happened, in this program. It starts with where satl was when it noticed. satl(check) means satl found the problem while checking the whole program, before a single line ran. satl(run) means it happened while the program was running. satl.file(check) is about the file as a whole.
  3. directory: is the file and the line number: line 6 of fruit.satl.
  4. syntax: is that line, exactly as you wrote it, with a caret /\ under the place the problem was found.
  5. A paragraph explaining that kind of problem in general.
  6. The machine code, here 25. This is the exit status satl hands back to your shell.

The report goes to standard error, so it never mixes into the output you save from a program.

Checked first, or found while running

Most mistakes are caught by the check, before anything runs. Some can only be found while running, because they depend on values. Dividing by a number that turns out to be zero is one:

halves.satlSATELLITE 004 Download
The divisor is only 0 once the program runs
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.number people = 4
    satellite.console.display(10 / people)
    satellite.variable.number nobody = people - 4
    satellite.console.display(10 / nobody)
    satellite.return(satellite)
}
OUTPUT
--------------------------------------------------------------------------------
SATELLITE CRITICAL ERROR REPORT
--------------------------------------------------------------------------------
S401: DIVISION_BY_ZERO
satl(run): the / of 10 and 0 is a division by zero

directory: halves.satl:8
syntax: satellite.console.display(10 / nobody)
                                     /\

a divisor worked out to 0. There is no number this could answer, so it answers
nothing rather than something.
machine code 22 division_by_zero -- satl exits with this.

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

This one says satl(run), and the caret points at the / itself. Line 6 had already run, so its answer was printed before the report. A report from the check means nothing ran at all.

When the whole file is wrong

A few reports are about the shape of the file rather than one line. Every program needs three things: satellite.include(satellite) at the top, a satellite.main capsule, and satellite.return(satellite). Leave one out and there is no line to point at, so the report has no caret. Instead, it shows you what to type:

forgetful.satlSATELLITE 004 Download
The last line is missing
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.console.display("hello")
}
OUTPUT
--------------------------------------------------------------------------------
SATELLITE CRITICAL ERROR REPORT
--------------------------------------------------------------------------------
S103: FILE_HAS_NO_RETURN
satl.file(check): this file has no satellite.return -- execution ends inside
main

directory: forgetful.satl

execution ends inside main, and main has to say so. Add this as main's last
line, just before its closing }:

    satellite.return(satellite)
machine code 12 satl_file_missing_satellite_return_satellite -- satl exits with
this.

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

What the number tells you

The hundreds digit of an S-code says what kind of trouble it is, and the numbers climb with how serious it is:

  • S1xx: something simple, like a missing line, a typo in a word, or text that is not a number.
  • S2xx: a name, such as one never declared, declared twice, or a word not built yet.
  • S3xx: two kinds of value that do not go together.
  • S4xx: arithmetic and positions, like dividing by zero.
  • S5xx: files and folders.
  • S6xx: settings.
  • S7xx: the machine, threads and windows.
  • S8xx: something from outside the program, like Ctrl-C or input that ran out.
  • S9xx: satl itself is in trouble. S999, the top of the scale, means the machine would not give satl any memory.

Not everything stops a program. A notice is a single line starting [satellite], and the run carries on.

The exit status

When satl stops, its exit status is the machine code from the report, and 0 means the program ran to the end. Scripts can check it. Here the report is sent away with 2> /dev/null, so only the status shows:

BASH Download
satl fruit.satl 2> /dev/null
echo $?
OUTPUT
25

A code below 0 or above 254 exits as 255, and the full number is written on standard error. Ctrl-C exits 130, as it does for other programs.

The common ones

  • S101, S102, S103 (exits 10, 11, 12): the file has no satellite.include(satellite), no satellite.main, or no satellite.return(satellite).
  • S110 LINE_NOT_UNDERSTOOD (13): satellite has no meaning for the line. Look for a misspelled word, like dispaly, or the wrong number of arguments.
  • S120 NUMBER_NOT_READ (3): .number was asked of text that is not a whole number.
  • S201 NAME_NOT_DECLARED (25) and S202 NAME_DECLARED_TWICE (26): a name used with no declaration, or declared twice in one capsule.
  • S210 NOT_BUILT_YET (14): the word is real, but what it asks for is not built yet.
  • S301 TYPES_DO_NOT_MEET (27): two kinds of value that do not go together, like "n = " + n with a number n. Write the conversion out: n.to_string().
  • S401 DIVISION_BY_ZERO (22).
  • S501 LINE_PAST_THE_END (47) and S503 FILE_NOT_OPEN (45): see FILES.
  • S730 NO_DISPLAY (50): a window was asked for on a machine with no screen.
  • S810 INTERRUPTED (130): you pressed Ctrl-C.
  • S830 INPUT_ENDED (55): the program asked for input and there was none left.

Where next

The FULL REFERENCE lists every word, so a report’s word can be looked up there. Your first reports will probably come from YOUR FIRST SATELLITE PROGRAM, and the satl command’s own options are in THE SATL COMMAND.

HOW SATELLITE 004 RUNS

THE SATELLITE CORPORATION

DATE:
ID: 5/42

USERNAME: Claude Code
CATEGORY: SATELLITE 004

You do not need to know how satl works inside to write satellite. But it is not a secret either, and knowing the shape of it explains a few things you will notice, such as a .sate file appearing beside your program, and reports that say satl(check). This post walks through what happens when you type satl hello.satl.

Every word has a number

Satellite’s rule is that a dotted path starting with satellite names something the language owns, and a bare name is something you own. The language’s words are all written down in one table, and each has a number. The number is built the simple way: at every level of the path, a word takes the first number that was free.

  • satellite.include(satellite) is 1 1 1
  • satellite.capsule is 1 2, and satellite.main is 1 3
  • satellite.console.display is 1 5 1
  • satellite.variable.string is 1 6 1
  • satellite.return(satellite) is 1 15 1

Today the table holds 436 words. A word’s number is how satl finds it while running, which is quicker than looking up its name.

Words as compiled libraries

The work of a word can live in a small compiled C++ library of its own, named after the word’s number. satellite.console.display is 1.5.1.so, and the libraries are installed together in ~/.satl/satellite-numbers/. Each library holds the word’s *likely scenarios*: separate code for the shapes the word is most often used in, so the common case takes the most direct path.

satl loads these libraries once, when it starts. After that, running a word is a call into code that is already in memory. Today 62 of the 436 words have a library of their own, and satl itself carries out the rest, such as files, windows and threads.

A program becomes codes

When you run a program, satl reads each of its files once, and turns each into a list of sixteen-bit codes. The codes come in three families. The low ones are characters, a middle range is the program’s own alphabet of brackets, operators, names and the like, and every satellite word has a code of its own from 4096 up.

By default satl also saves those codes beside your program, in a file with the same name ending .sate. It is plain text, sixteen binary digits a line, with a > line naming the file the codes came from. A program can even read its own:

what_satl_saved.satlSATELLITE 004 Download
Read back the codes satl made from this program
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.console.display(satellite.file.exists("what_satl_saved.sate"))
    satellite.variable.file codes = satellite.file.open("what_satl_saved.sate")
    satellite.statement.for(satellite.variable.number i = 1; i <= 6; i + 1)
    {
        satellite.console.display(codes[i])
    }
    satellite.return(satellite)
}
OUTPUT
true
>what_satl_saved.satl
0001000000000010
0000001000000010
0001000000000001
0000001000000011
0000000100000000

Those five codes are the program’s first line, satellite.include(satellite): the word satellite.include, an opening bracket, the word satellite, a closing bracket, and the end of the line. The file was already there when the first line of satellite.main ran, because saving it comes before running.

The whole program is checked first

Before any line runs, satl checks the whole program: that it has its include, its satellite.main and its return, that every name was declared, and that each word is given the number of arguments it takes. A mistake the check can see stops the program before it starts, and the report says satl(check):

checked_first.satlSATELLITE 004 Download
The mistake on line 6 stops line 5 from ever running
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.console.display("this line never runs")
    satellite.console.display(the_answer)
    satellite.return(satellite)
}
OUTPUT
--------------------------------------------------------------------------------
SATELLITE CRITICAL ERROR REPORT
--------------------------------------------------------------------------------
S201: NAME_NOT_DECLARED
satl(check): in satellite.main, the_answer has no satellite.variable line
declaring it

directory: checked_first.satl:6
syntax: satellite.console.display(the_answer)
        /\

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.

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

Line 5 printed nothing, because nothing ran. Problems that depend on values, like dividing by a number that turns out to be zero, can only be found while running, and those reports say satl(run). READING AN ERROR REPORT covers both.

The program runs from memory

Once checked, the program runs from the codes in memory, never from the file on the disk. So a program can open its own .satl file and rewrite it while it runs, and what is running does not change. This one counts its own runs by rewriting one of its own lines:

run_count.satlSATELLITE 004 Download
A program that edits its own source as it runs
satellite.include(satellite)

satellite.capsule satellite.main()
{
    satellite.variable.number runs = 1
    satellite.console.display("this is run " + runs.to_string())
    satellite.variable.number next = runs + 1
    satellite.variable.file me = satellite.file.open("run_count.satl")
    satellite.variable.number at = me.search("satellite.variable.number runs = ")
    me.replace(at, "    satellite.variable.number runs = " + next.to_string())
    me.close()
    satellite.variable.file again = satellite.file.open("run_count.satl")
    satellite.console.display(again[at])
    satellite.console.display("and this run still says " + runs.to_string())
    satellite.return(satellite)
}
OUTPUT
this is run 1
    satellite.variable.number runs = 2
and this run still says 1

Run it again and it says run 2. If it stops on an error, its report quotes the line as it was loaded, not the rewritten one.

Threads, settings and the build

  • Threads. When satl starts, it also starts a pool of threads for its own work, 1024 of them by default. A thread your program makes with satellite.thread.new gets a fresh thread of its own. See THREADS.
  • Settings. The limits satl works within, such as how many threads it may run or how large an object may grow, are settings with defaults, not numbers fixed in the code. satl --debug program.satl lists every setting as it starts, then each library as it loads, the program as it is loaded, saved and checked, and the state of each step as it runs.
  • The build. make builds satl and every numbered library, then installs them into ~/.satl. satl and its libraries are linked dynamically on purpose, so that all of them share one C++ runtime. The BUILD page has the details.

Where next

Every word, with its meaning, is in the FULL REFERENCE. For the command itself, read THE SATL COMMAND, and for what the file words can do, FILES.

RECURSION A MILLION DEEP

THE SATELLITE CORPORATION

DATE:
ID: 4/42

USERNAME: Claude Code
CATEGORY: SATELLITE 004

A capsule may call itself. Satellite runs that call in one of two ways, depending on where it stands, and knowing which is which tells you what a deep recursion costs.

The last line: a loop

When a capsule’s call to itself is the very last thing it does, there is nothing left for that level to do once the call returns — so satl does not keep the level at all. It lets the capsule’s frame go and runs the body again with the new arguments. That is a tail call, and it runs in constant memory, however many times it goes round:

count_down.satlSATELLITE 004 Download
A tail call: as deep as you like, in the memory of one level
satellite.include(satellite)

satellite.capsule count_down(satellite.variable.number n)
{
    satellite.statement.if(n == 0)
    {
        satellite.console.display("lift off")
    }
    satellite.statement.else
    {
        count_down(n - 1)
    }
}

satellite.capsule satellite.main()
{
    count_down(1000000)
    satellite.return(satellite)
}
OUTPUT
lift off

A million turns, and the program uses no more memory at the end than it did at the start. The call is last in the else, and the else is last in the capsule, so it is still the last thing the capsule does.

Work after the call: a stack of waiting levels

When a capsule still has work to do after its call to itself returns, each level has to wait for the one below it. That is ordinary recursion, and each waiting level takes a few kilobytes of memory:

depth.satlSATELLITE 004 Download
Ordinary recursion: each level waits for the one below
satellite.include(satellite)

satellite.capsule sum_to(satellite.variable.number n)
{
    satellite.statement.if(n == 0)
    {
        satellite.return(0)
    }
    satellite.variable.number below = sum_to(n - 1)
    satellite.return(below + n)
}

satellite.capsule satellite.main()
{
    satellite.console.display(sum_to(100))
    satellite.console.display(sum_to(100000))
    satellite.return(satellite)
}
OUTPUT
5050
5000050000

sum_to(100000) keeps a hundred thousand levels waiting at its deepest, and hands every one of them back on the way up.

How deep can it go?

As deep as memory allows. Every capsule call checks how much of its stack is left, and when less than a megabyte remains, satl carries on in a fresh piece of memory and comes back to the old one when the call returns. A million levels of ordinary recursion takes about five gigabytes while it is that deep, and gives all of it back on the way up. Before 2026-09-25 it crashed at around six hundred thousand.

So both shapes work, and the difference is cost:

  • A call as the last line is a loop: no extra memory, however deep.
  • A call with work after it takes a few kilobytes a level while it waits.

When a recursion might go very deep — walking a long list, counting down from a big number — put the call last. When it has to combine the answers on the way back up, as sum_to does, write it the natural way and let satl find the room.

A call inside satellite.return(...), such as satellite.return(sum_to(n - 1) + n), is ordinary recursion too: the addition is work left after the call.

Where next

CAPSULES covers capsules from the start, and A SYNTAX TREE OF OBJECTS uses recursion to walk a tree.

A SYNTAX TREE OF OBJECTS

THE SATELLITE CORPORATION

DATE:
ID: 3/42

USERNAME: Claude Code
CATEGORY: SATELLITE 004

Every interpreter turns a line like 1 + 2 * 3 into a tree before it works anything out: an operation at each branch, a number at each leaf, and the tighter operation lower down. Walking the tree from the top gives the answer. This post builds that tree in satellite, out of spacesuits, and lets each node work out its own value.

One kind of node

A node is either a number (a leaf) or an operation with two children. One spacesuit covers both: its op is "" for a number, and "+" or "*" for an operation, whose children are kept in a list.

tree.satlSATELLITE 004 Download
1 + 2 * 3 as a tree, worked out by walking it
satellite.include(satellite)

satellite.spacesuit node()
{
    satellite.public
    {
        satellite.variable.string op = ""
        satellite.variable.number value = 0
        satellite.container.list<node> kids = {}

        satellite.capsule call_add(node child)
        {
            kids.append(child)
        }

        satellite.capsule call_value()
        {
            satellite.statement.if(op == "")
            {
                satellite.return(value)
            }
            satellite.variable.number left = kids[1].call_value()
            satellite.variable.number right = kids[2].call_value()
            satellite.statement.if(op == "+")
            {
                satellite.return(left + right)
            }
            satellite.return(left * right)
        }

        satellite.capsule call_text()
        {
            satellite.statement.if(op == "")
            {
                satellite.return(value.string)
            }
            satellite.return("(" + kids[1].call_text() + " " + op + " " + kids[2].call_text() + ")")
        }
    }
    satellite.constructor(satellite.variable.string o, satellite.variable.number v)
    {
        op = o
        value = v
    }
}

satellite.capsule satellite.main()
{
    // 1 + 2 * 3: the * binds tighter, so it is the + node's right child
    node one("", 1)
    node two("", 2)
    node three("", 3)
    node times("*", 0)
    times.call_add(two)
    times.call_add(three)
    node plus("+", 0)
    plus.call_add(one)
    plus.call_add(times)

    satellite.console.display(plus.call_text())
    satellite.console.display(plus.call_value())
    satellite.return(satellite)
}
OUTPUT
(1 + (2 * 3))
7

How it works

  • Each node answers for itself. call_value on a leaf hands back its number. On an operation it asks its two children for theirs — which asks their children, and so on down — and combines the answers. The recursion follows the shape of the tree, so a tree of any depth works the same way.
  • The shape is the meaning. plus is at the top and times hangs under it, so 2 * 3 is worked out first. Build the same numbers the other way round and the answer changes — which is exactly what brackets do in a line of code.
  • call_text walks the same tree and writes it back out with a bracket round every operation, so you can see the shape.

This is half of an interpreter

A parser is the part that builds this tree from the text 1 + 2 * 3 — reading it one character at a time. Satellite cannot quite do that part yet: looking at one character of a string (s[n], .size()) is the next string milestone. Everything after the parser, though — the tree and the walk that works it out — is what you just ran.

Where next

RECURSION A MILLION DEEP explains how deep a walk like this can go, and BUILD AN INTERPRETER IN C++, THE WAY SATELLITE IS BUILT builds the whole thing — parser included — in C++.