Shoddy Documentation

The Toolchain

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.

Most people don't need this page. The VS Code extension ships a mill and the whole machine library inside it, so writing Shoddy takes no toolchain install at all (Setup, Track A). Read on if you're working on the language itself, or want mill on a command line.

1. The mill's commands

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.

2. The build wrappers

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.

3. Rebuilding the mill

./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/.

4. Includes and machines

5. The linter

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:

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.

6. Errors and testing

(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:

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.