Shoddy Documentation
The machine under the costume — watched, one word at a time.
The guide's chapter 7
reveals that every Shoddy program compiles onto a stack machine, and the
spec's Appendix A
states its laws. This page is the part in between: the machine actually
running, with the stack drawn after every word. It ends with a
full tour of EvenSquareSum. Every example here was run
through the mill (Shoddy's command-line tool). You can paste any of them
and watch for yourself.
The whole machine is a single pile — the stack. A program is a sequence of words, executed left to right, top to bottom. Each word does exactly one kind of thing: take however many values it needs off the top of the pile, and leave its results on the top of the pile. Nothing else. No statements, no expression tree, no operator precedence (rules about which operation goes first). By the time the machine runs, all of that has been compiled away into an order of words.
Every word documents what it eats and what it leaves in a stack-effect signature. Read one left to right, with the top of the stack rightmost:
( Number -- Number ) ' eats one number, leaves one number
( a b -- b a ) ' eats two values, leaves them swapped
( List Quotation -- List ) ' what Filter and Map look like
In every diagram on this page, the stack is drawn the same way: bottom on the left, top on the right. New values arrive on the right; words eat from the right.
The guide's two-word squaring function:
Def Square ( Number -- Number )
Dup *
Call Square(7) — which the mill compiles to
7 Square — and watch the pile:
| Word | Stack after | What happened |
|---|---|---|
| 7 | 7 | a literal pushes itself |
| Dup | 7 7 | copy the top value |
| * | 49 | eat the top two, leave their product |
That's the entire trick of the two-word body. * needs two
values and we only have one, so Dup manufactures the second.
The function's result is simply whatever is on the stack when its body
ends.
One size up — the hypotenuse, the longest side of a right-angled
triangle. Hypot(3, 4) compiles
to 3 4 Hypot:
Def Hypot ( Number Number -- Number )
Dup * Swap Dup * + Sqr
| Word | Stack after | What happened |
|---|---|---|
| 3 | 3 | |
| 4 | 3 4 | arguments load left to right — last on top |
| Dup | 3 4 4 | |
| * | 3 16 | 4², done |
| Swap | 16 3 | bring the 3 to the top — its turn |
| Dup | 16 3 3 | |
| * | 16 9 | 3², done |
| + | 25 | |
| Sqr | 5 | the square root, and the answer |
Read the body again — Dup * Swap Dup * + Sqr — and it
narrates itself now: square the top, swap, square the other, add, root.
Squaring needed Dup. The hypotenuse needed
Swap. They come from a small standard family of
shuffle words, each rearranging the top of the pile and
nothing more:
| Word | Effect | Before → after |
|---|---|---|
| Dup | ( a — a a ) | 1 2 → 1 2 2 |
| Drop | ( a — ) | 1 2 → 1 |
| Swap | ( a b — b a ) | 1 2 → 2 1 |
| Over | ( a b — a b a ) | 1 2 → 1 2 1 |
| Rot | ( a b c — b c a ) | 1 2 3 → 2 3 1 |
| Nip | ( a b — b ) | 1 2 → 2 |
| Tuck | ( a b — b a b ) | 1 2 → 2 1 2 |
| Depth | ( — n ) | 1 2 → 1 2 2 (pushes the count) |
Take is the escape hatch, for when three values need
rearranging and the shuffle spelling stops being fun. It pops values into
immutable named bindings — names that hold a value and never change. The
topmost value goes to the rightmost name. After that you just say
the names in whatever order you please:
Def Middle ( a b c -- b )
Take x, y, z ' z was on top, x deepest
y
Take is the exit from Swap Rot Dup
shuffle-hell — Appendix A calls it exactly that. Use shuffle words when
the dance is one or two steps. Use Take the moment you have
to stop and count.
One more kind of value can sit on the stack: code that hasn't
run yet. A value like that is called a quotation — code
in a box, saved to run later. Brackets make one: [ Square ]
pushes a package containing the word Square, without running
it. Words like Map, Filter, Fold,
Call, and Ifte eat a quotation and run it, on
their own terms. Words that do this are called combinators —
ordinary words whose job is to run the code you hand them. Map runs it
once per item, Ifte runs one of two, Call just runs it.
And here is the neatest fact in the whole core: a list is a
quotation of literals. The surface's { 1, 2, 3 }
compiles to exactly [ 1 2 3 ] — a package of code that, run,
would push 1, 2, 3. A list and a quotation are the same thing: run a list
and it pushes its items. (The Joy language calls this property
homoiconicity; Shoddy inherits it whole.) That's why this page
draws lists and quotations the same way in the traces below. They are the
same kind of thing on the pile.
Now the showpiece from the guide and the spec — sum of the squares of the even numbers up to N:
Def EvenSquareSum(n As Number) As Number
Range(1, n)
Filter(IsEven)
Map(Square)
Fold(0, +)
What does the mill make of this? Three desugarings, all mechanical. (A
desugaring is a rewrite from the friendly spelling into the plain one.)
The parameter becomes a Take. Each call f(a, b)
becomes a b f. And each bare function name in argument
position — IsEven, Square, + —
becomes a quotation. The indented lines are pipeline chaining:
each call's missing first argument is simply whatever the previous line
left on the stack. Flattened, the body is:
Take n
1 n Range [ IsEven ] Filter [ Square ] Map 0 [ + ] Fold
(Run it yourself — that spelling compiles and returns the same answers
as the sugared one.) Here is the whole execution at n = 4,
stack drawn after every word:
| Word | Stack after | What happened |
|---|---|---|
| Take n | the argument 4 leaves the pile, named n | |
| 1 | 1 | |
| n | 1 4 | a binding pushes its value |
| Range | [ 1 2 3 4 ] | eats both bounds, leaves one list |
| [ IsEven ] | [ 1 2 3 4 ] [ IsEven ] | a quotation — pushed, not run |
| Filter | [ 2 4 ] | eats list and test, runs the test per item |
| [ Square ] | [ 2 4 ] [ Square ] | |
| Map | [ 4 16 ] | runs Square on each item |
| 0 | [ 4 16 ] 0 | the fold's starting accumulator |
| [ + ] | [ 4 16 ] 0 [ + ] | |
| Fold | 20 | eats all three, leaves the sum |
Notice the shape of the whole thing. The pile never gets more than three deep, and between stages it holds exactly one value — the data, flowing left to right. That's why the surface version reads as a pipeline: each line's result is just sitting there, and the next line's word consumes it as its first argument. The stack isn't an implementation detail the pipeline hides. The stack is why the pipeline works.
Fold looks like one step in the trace, but it runs its
quotation once per item. Each result feeds back in as the next
accumulator — the running total so far. On Fold's own private beat:
| Iteration | Runs | Accumulator after |
|---|---|---|
| start | 0 | |
| item 4 | 0 4 + | 4 |
| item 16 | 4 16 + | 20 |
Each iteration is itself just the machine again: push the accumulator,
push the item, run the quotation — which here is one word, +.
Combinators aren't magic. They're loops that push things and run your
quotation.
You don't have to trust this page's tables — the toolchain will draw the pile for you, two ways:
.shoddy file in VS
Code and press F5. The debugger — the tool that runs your program one step
at a time — shows a Value Stack panel, and it is exactly
this page's diagrams, live. Set a breakpoint (a marked line where the
program pauses) in a concatenative (stack-style) def —
Hypot above is a good first victim — and step with F10,
watching values arrive and leave. (The VS Code
page has the tour.)mill gen file.shoddy
(or Shoddy: Show Generated C# in the editor) prints what the weave
(the compile step) emits. You can see the pushes and calls the trace
tables describe.Leave the parentheses off a Def header and the body is
stack code — that's the whole switch:
Def Square ( Number -- Number ) ' concatenative: effect signature
Dup *
Def Square(n As Number) As Number ' applicative: same word, other costume
n * n
The two dialects mix on any line, in both directions. Each has its
home ground. Stack style (the technical name is point-free) wins
when data genuinely flows in a line — pipelines, wrappers, one-liners like
Dup * where a variable would be ceremony. Named-argument
style (applicative) wins the moment logic branches or three
things need juggling. Two honest gotchas come from mixing them, and both
will bite you exactly once:
Str(a) & Str(b) there and the
& runs with only Str(a) on the pile. That is
a stack underflow: the pile holds fewer values than the word needs. Push
both first (a Str b Str &), or Take your
values and finish the job in an applicative helper.Print(Square) doesn't call Square — it prints the quotation
[ SQUARE ]. A bare name given as an argument is passed as
code. That rule is what makes Map(xs, Square) work. If you
meant to call it, give it its arguments — Print(Square(7)).
For a zero-argument name, force evaluation with an extra pair of
parentheses: Print((Pi)) prints 3.14159…, where
Print(Pi) would print [ PI ].The law lives in Appendix A — one page, worth reading now that you've watched the machine it governs.
Everything on this page happens at weave time, while the program is
being compiled. The pile you've been watching is compiled away, and a
running Shoddy program never holds it as a value.
cuttle lets a running program hold a
stack of its own as a value. It is that same model reified — made into a
real thing the program can touch: a heterogeneous Stack (one
that holds values of any type) that a mill can push onto, pop and inspect
at runtime. The shuffle words above (Dup, Swap,
Rot, and the rest) become an ordinary pure algebra over it —
plain functions from stack to stack — rather than compiler machinery.
reckoner lets you type stack code as
a line of text and watch it run. It goes one step further and puts an
evaluator in front of that stack, so that typed RPN lines like
3 4 + run against a dictionary — the set of words the
evaluator knows — the way this page's traces run against the compiled
stack. (RPN is Reverse Polish Notation: exactly this page's order, values
first, then the word.) Thirteen seed files bridge the rest of the
standard library into that dictionary.
reckoner's own page lists
them.
If you would rather watch it than read it, the halifax calculator's TRACE mode prints these same traces live — one row per token, for whatever you type at its prompt. It indents each row by how deep the sub-evaluation is, because a program running on a fresh stack is a different pile from the one it was called on.