Shoddy Documentation
The mill from the command line — every command, wrapper, and convention.
This is the reference for working directly with the mill — the program that compiles and runs Shoddy. It covers the mill's commands, the repo's build wrappers, how weaving and machines fit together, and the testing rules. It assumes a mill built from source and on your PATH, the list of folders your terminal searches for commands — Setup, Track B.
mill on a command
line.mill run x.shoddy # compile in memory and run (default: mill x.shoddy)
mill weave x.shoddy # compile -> x.dll (+ runtimeconfig + runtime dlls)
dotnet x.dll # run the woven program
mill machine lib.shoddy # compile a library to a machine DLL
mill gen x.shoddy # print the generated C#
mill lex x.shoddy # dump token lines (debug)
mill dap # DAP server — the perch (the editor launches this)
mill run --allow-net x.shoddy # arm the gated TCP builtins (the net machine)
mill run --no-window x.shoddy # open every scribbler hidden (a capture run)
mill run and mill weave are the same compiler
with a different destination. Weaving is Shoddy's word for compiling — turning
your source into a runnable program. run weaves to memory and
executes. weave writes the assembly — the compiled
.dll file — to disk. Everything is
compiled; there is no interpreter, no program reading your source line by
line at run time. mill run FILE args...
passes the remaining arguments through to the program.
One platform note: programs that open a scribbler window — Shoddy's
drawing window — must run under
mill run. A woven dotnet FILE.dll has no window
backend. Console programs weave and run anywhere .NET runs.
--no-window opens every scribbler hidden: the program
draws, blits and ScribblerSaves exactly as usual, but nothing
reaches the screen. It is for a run whose output is a file — a chart captured
by a build, a screenshot in a script — where a window that flashes up and
vanishes is noise. It also stops windows outliving the program. Normally
"draw a picture and return" leaves the window up until you dismiss it, and
under this flag there is no window anyone could dismiss. Note that hidden is
not headless — headless means a machine with no display at all. GLFW, the
window library, still has to initialize, so a machine with no display
cannot open a scribbler either way.
Networking is a gated capability — a feature that stays switched off until
you arm it. The nine TCP* builtins (and
the net machine built on them) abort unless the
mill is run with --allow-net. The flag may appear anywhere on the
command line. It sets SHODDY_ALLOW_NET=1 in the environment — the
named settings a program inherits when it starts — so a
woven standalone program (dotnet x.dll) honours the same switch.
Set that variable yourself when running one.
Thin convenience wrappers — small scripts that bundle the common commands —
sit at the repo root:
build.ps1 on Windows, build.sh on
Linux/macOS/WSL, same subcommands:
./build.sh build # build the mill into bin/
./build.sh test # the whole suite: conformance, machines, mills
./build.sh check # the seven fast verify gates (read-only)
./build.sh run tst/examples.shoddy
./build.sh weave tst/simplex.shoddy
./build.sh machines # compile every machine to a DLL
./build.sh stage # stage the mill + machines into the extension
./build.sh vsix # package the VS Code extension (.vsix)
./build.sh vsix patch # bump version, then package
./build.sh install # install the packaged .vsix into VS Code
./build.sh all # clean, test, then vsix -- the ship path
./build.sh clean
On Windows, run ./build.ps1 in place of
./build.sh (e.g. ./build.ps1 build). If PowerShell
refuses the unsigned script, bypass the policy for that one invocation:
powershell -ExecutionPolicy Bypass -File .\build.ps1 build.
test is the proof: the C# conformance suite, every core and
machine suite, and every mill's own suite, in one run (about twenty
minutes). check is its cheap companion — seven read-only
verify gates (docs, errors, permissions, host-blind, suites, twins, lanes)
that finish in seconds; CI runs both on every push. Three suites that need a
real display are excluded from test and run by
./scripts/shoddy-display.ps1 (./scripts/shoddy-display.sh);
-List names them. The maintainer flow around all of this —
branch, prove, pull request, ship — lives in the repo itself:
WORKFLOW.md
is the sequence and
RELEASING.md
the reasoning behind it.
Each mill under mills/ carries the same pair with the same
spirit — build.sh and build.ps1, the same
subcommands in each. So a mill can be built and tested from whichever
shell you already have. See the mills
catalog.
./build.sh build runs
dotnet publish src/Shoddy.Mill -c Release -o bin. That is a folder
publish, not single-file, and the difference is load-bearing: the weaver
references
Shoddy.Runtime.dll from disk when compiling generated code.
The executable lands at bin/mill (bin\mill.exe on
Windows). Re-run it after touching anything under src/.
Include "FILE.shoddy" resolves relative to the including file, then
the SHODDYLIB directory, then the machine library shipped with the
running mill (machines/ beside the executable, then beside its
bin/). A bare Include "seq.shoddy" therefore works from any
folder with no setup — which is how the mills spell it. Include-once: a
file arrives at most once.
A machine is always used compiled. Every entry point that
reads a program — mill run, mill weave, and the
debugger — links against Shoddy.Machines.<Name>.dll beside
the .shoddy, and builds it first if it is missing or older than
its source. What a program means never depends on what happens to have been
built. Splicing a machine instead would put its own includes into your flat
table and quietly undo the surface scoping. A file that is not in a
machine library — one of your program's own source files — splices as it
always has. That is what lets a mill spread itself across a dozen files.
./build.sh machines still compiles the lot up front, working out
the order itself: it reads each machine's Include lines and builds
a machine only once everything it includes is already a DLL
(seq before dict, matrix before
simplex, and so on). To build one by hand —
bin/mill machine machines/seq.shoddy
Curious what your program compiles to? mill gen file.shoddy
prints the generated C#. Self-tail-recursive defs show
continue; // self tail call — the loop is the TCO. (TCO is
tail-call optimization: the compiler turns that recursion into a loop.)
Stale DLLs rebuild themselves. When a machine's
.shoddy source (or any machine DLL it depends on) is newer than its
DLL, the mill rebuilds it in place before loading — with a one-line note on
stderr. An edited machine can no longer be silently shadowed by yesterday's
build.
Every mill run, weave, and machine lints the parsed
program before anything executes. Linting is an automatic check that warns
about likely mistakes. The doctrine: warnings, never
errors, and only on the provable. Warnings go to
stderr — the separate stream for error text — never to stdout (the normal
output) and never into the exit code, the number a program returns to say
whether it succeeded. A Def the checker cannot model (one that reaches
Call on a quotation of unknown effect, say) is skipped, not guessed;
--lint-verbose prints how many Defs were checked so coverage
stays visible. --no-lint (or SHODDY_LINT=0) silences the
warnings.
What it checks:
Include earns its place — one contributing no
word or type the file names is reported, now that an include is a statement
about what you use rather than an incidental splice;Def's net stack effect (one value, or none);If and Select Case arm agree;Let does not shadow a field accessor the program
actually reads;And/Or;The one error: an unknown word. Nothing legitimate survives resolution without a meaning, and the same program would die at runtime with less context — so the weave stops, and no flag overrides it.
Every message the linter can print — alongside every weave and runtime error — is cataloged with its cause and fix on the errors page.
(The full catalog — every message, its cause, its fix — is the errors page; this section is the shape of the system.)
Errors print to stderr and exit 1, in one of three shapes. A compile-time
error that can point across an Include boundary — a duplicate
definition, a shadowed builtin, a bad Def header — names the
file: ERROR (file.shoddy:N): message. A runtime error names only
the line: ERROR (line N): message — the line number is within the
file that defines the failing code. An error with no source line at all, such
as a duplicate export across machine DLLs, prints bare
ERROR: message. Assert(cond, "MSG") is built
in.
The conformance rule of the whole project: tst/golden/ is the
constitution. The goldens are saved known-good outputs that every test run
must reproduce exactly. After touching anything under src/, run
dotnet test src/Shoddy.Tests for the quick conformance check —
the five goldens compiled and run in-process, the machines path, and the
TCO check, all byte-identical — or ./build.sh test for the
whole suite, which begins with exactly that and carries on through every
machine and mill. Green means the mill is honest.
The subtlest warning deserves its own explanation: accessor shadowing.
Declaring a Type puts a field accessor in scope everywhere the
type is visible, often from another file. And an accessor loses every naming
contest, because a word resolves through local binding, top-level
Let, program Def, machine Def, type
constructor, builtin, and only then accessor. So a mid-body
Let landed = … does not read like a shadowing declaration, it
reads like a variable — and the accessor it hides simply stops being reachable
in that scope. (This chain decides what a word means once
declarations are in place; how a name finds its declaration across namespaces
is the separate resolution described in
spec §13.1.) Nothing else catches this.
Annotations are runtime-only, and the shadowing name almost always has the
type the accessor would have returned, so there is no error at run time
either. The program just computes the wrong answer.
mill.shoddy:5: warning: 'LANDED' shadows field accessor 'Landed'
of type 'Census' — the accessor is unreachable in this scope
Three things are deliberately quiet:
x and a y, and misuse inside such a
scope is the stack checker's to catch; only a mid-body Let
warns.Died(Msg), KNumeric(N) — so its accessor
is not worth defending.The mirror case — a Def or machine word taking
the same name as a field accessor — is warned at the definition that creates
the collision, whenever the program actually writes the contested word.
The language itself is documented in the guide, the spec, the quick reference, and the machine pages; this page is only the loom-side of the workshop.