GuideThe Silo HandbookStack and Words

Chapter 1 Stack and Words

Source

Silo programs are pipelines of values flowing through a stack. You push values, call words that consume them and push results, and chain the whole thing together. There are no parentheses around function arguments, no infix operators, no implicit precedence — everything is postfix and left-to-right.

This chapter introduces the stack, the literal forms, the rules for naming things, and the two pieces of syntax you'll use to bind intermediate values: pop-> and peek->.

The stack

Silo has a single data stack that every word reads from and writes to. A literal like 42 pushes itself. A word like + pops two values and pushes their sum. Reading a program is just simulating what the stack looks like as you move through the source:

42                           # ⌊42⌉
3 4 +                        # ⌊42 7⌉
2 3 * 1 +                    # ⌊42 7 7⌉

(The floor-and-ceiling brackets ⌊…⌉ are how the REPL draws the stack. The deepest value is on the left; the top of the stack — the one that drop would remove — is on the right.)

The stack is typed at compile time. The checker knows the shape of the stack at every point in your source and rejects any word call whose inputs don't match what's actually on the stack. You never see the stack mentioned in a type error the way you see a line number in a Python error — it's more like a Rust type mismatch, but the "types" include their position in the pipeline.

Stack shufflers

Silo ships a small set of built-in stack shufflers for rearranging the top few values on the stack. You'll see these in short pipelines; for anything longer, reach for pop-> and peek-> (covered later in this chapter) because named bindings read better than a string of shufflers.

Word Stack effect What it does
dup a → a a Duplicate the top
drop a → Discard the top
swap a b → b a Swap the top two
over a b → a b a Copy the second-from-top to top
rot a b c → b c a Rotate three: second becomes top
nip a b → b Drop the second-from-top

These are all polymorphic — the stack-effect variables a, b, c stand for any type. For heap values, dup creates a new logical reference; the compiler's reference counter tracks the rest. That's covered in the lifecycle chapter.

Every shuffler has an arity variant written with a /N suffix. The variant operates on groups of N consecutive values instead of single values, so you can rearrange several values in one move instead of chaining primitives. The /2 family covers pairs:

Word Stack effect What it does
dup/2 a b → a b a b Duplicate the top pair
drop/2 a b → Discard the top pair
swap/2 a b c d → c d a b Swap the top two pairs
over/2 a b c d → a b c d a b Copy the second pair to the top
nip/2 a b c d → c d Drop the second pair
rot/2 a b c d e f → c d e f a b Rotate three pairs

So 3 5 dup/2 leaves 3 5 3 5 on the stack — useful when a pair needs to flow into two places. The bare dup is equivalent to dup/1; the bare forms are just the arity-1 shorthand.

Literal forms

Numbers come in four representations, split along one important axis: exact versus inexact.

# Integers — exact, arbitrary precision. No overflow.
42
-7
0xFF
0b1010
2 100 pow                    # a very large integer, still exact

# Fractions — exact rationals.
3/4
1/3 1/4 +                    # 7/12, not 0.5833...

# Decimals — exact decimal, unbounded.
19.99:D64
0.1:D128                     # exactly 0.1 — no binary rounding error

# Floats — IEEE 754 binary. Inexact by construction.
3.14                         # F64 by default
0.1                          # NOT exactly 0.1

Mixing an inexact value with an exact one contaminates: the result is inexact. That way you always know whether a number went through a Float at any point.

The other primitive literals:

"hello world"                # UTF-8 string
"tab:\there\n"               # escapes: \n \r \t \\ \" \0
"\u{1F600}"                  # Unicode escape

'a'                          # a single codepoint (the Char type)
'\u{0301}'                   # combining accent — also a codepoint

true false                   # Bool

'ok                          # a symbol — a globally interned atom
'error                       # with O(1) equality

0..10                        # a range, 0 inclusive to 10 exclusive
5..                          # from 5 onward
..10                         # up to 10
..                           # unbounded

b"raw bytes"                 # a byte string

Symbols are Lisp-style atoms: two 'ok literals anywhere in the program compare equal in constant time, and they carry no payload. They're useful when you want a cheap, distinct tag without reaching for a full enum.

Identifiers

Identifiers in Silo are liberal: Unicode letters to begin, then letters or digits or a handful of punctuation characters. Hyphens are part of the identifier — my-function is one name, not a subtraction. The punctuation ?, !, ', /, <, > is all valid, as are Unicode symbols in general — math operators like , , , arrows like , and any other symbol character your domain calls for:

my-function                  # hyphens
is-valid?                    # trailing ?
u8>int                       # angle brackets in a conversion name
str>bytes                    # another conversion
m'                           # trailing apostrophe

Unicode math symbols are valid word names too. Silo's set-theoretic traits use them directly, so a b ∪ really does mean set union:

a b# union
a b# intersection
x s# membership
p q# subset

Non-Latin scripts work anywhere a Latin-script word would:

土豆 pop-> p
čuõʹnnj pop-> g
μαθηματικά pop-> m

There's nothing special about these examples — they're ordinary identifiers under Unicode's XID_Start and XID_Continue rules, plus the extra punctuation Silo allows. Use whatever reads best for the domain you're modelling.

The convention — not the rule — is:

  • Lowercase starts a word (a function, a local binding).
  • Uppercase starts a type or a constructor.

So Point is a type and point is a variable holding one. This is the same convention Haskell uses.

Words

A word is a function. You declare one with :fn:

:fn square ( Int -> Int )
  dup *
:end

The signature ( Int -> Int ) is the stack effect. It reads "pops one Int, pushes one Int." The body is ordinary Silo code. dup duplicates the top of the stack, and * pops two values and pushes their product.

Call a word just by naming it:

5 square                     # 25

Stack effects can consume or produce multiple values. Here's a word that takes two integers and produces one:

:fn add ( Int Int -> Int )
  +
:end

3 4 add                      # 7

The compiler checks every call against the signature. If you try to call square with a Str on top of the stack, the program doesn't compile.

Local bindings

Threading values through a stack works for small words but gets unreadable quickly. For anything beyond a line or two, you'll bind intermediate values to names. The two forms are:

pop->   name                 # pops the top of the stack into `name`
peek->  name                 # copies the top into `name`, leaves it on the stack

Bindings are lexically scoped to the enclosing word or quotation, and shadowing is allowed. Here's a word that computes Euclidean distance between two points:

:fn distance ( F64 F64 F64 F64 -> F64 )
  pop-> y2 pop-> x2 pop-> y1 pop-> x1
  x2 x1 - peek-> dx
  y2 y1 - peek-> dy
  dx dx * dy dy * + sqrt
:end

A couple of things to notice here:

  • The pop-> bindings appear in reverse order of the signature. The signature says the arguments go on left-to-right, so the last one pushed is on top, and that's the first one popped.
  • peek-> is useful when you want both the name and the value still on the stack — here dx stays on the stack so the next operation can use it, and is also bound to the name dx for readability.

Shadowing is fine:

:fn example ( Int -> Int )
  pop-> x
  x 2 * pop-> x              # inner x shadows the outer x
  x 1 +
:end

Key points

  • A program is a stream of values flowing through a single typed stack.
  • Everything is postfix. No infix operators, no precedence rules.
  • Four numeric representations — Int, Fraction, Decimal, Float — with exact/inexact contamination rules that keep you honest.
  • Identifiers can contain hyphens, ?, !, Unicode — so is-sorted? and u8>int are single names.
  • Every word carries a stack-effect signature; the compiler uses it like a type signature.
  • pop-> moves a value off the stack into a name; peek-> copies without consuming. Both are lexically scoped.

The next chapter covers control flow — conditionals, pattern matching, loops, early returns, and error short-circuiting.