Shoddy Documentation
Highlighting, snippets, debugging and mill commands — now with a Run button
This page covers everything about writing Shoddy in VS Code: installing the extension, the tasks, native debugging (the perch — Shoddy's debugger), and the one environment wrinkle worth understanding. An extension is an add-on package that teaches VS Code new tricks — here, a whole language. This one carries its own mill (the program that compiles and runs Shoddy) and machines, so the only prerequisite is the .NET 10 runtime — Setup, Track A, two steps.
Only two sections here ask you to do anything: installing the extension (required, once) and the smoke test (recommended, a minute). Everything else is optional with its trigger stated on the section, plain usage (§3–§4, §6), or background (§5).
Key note. Shortcuts are written
Cmd+…/Ctrl+…— macOS usesCmd, Windows and Linux useCtrl. Everything else on this page is identical on every platform.
Shoddy has its own extension, and it provides:
Rem/' comments, all case-insensitive);def,
select, fn, ...);F5 (section 3);ctrl+r, plus the Γû╢ button), Weave File, Build Machine from
File, and Show Generated C#.It also carries a complete toolchain — every tool needed to build and run
Shoddy. A mill and every machine ride inside the package, so installing it is
the whole install. Download the latest .vsix from
the latest release
(one package, every platform), then in VS Code: Extensions view →
··· menu → Install from VSIX… → pick the file you
downloaded. Reload when prompted.
The bundled mill is portable .NET: mill.exe on Windows, and
dotnet mill.dll everywhere else, through the runtime
Setup had you install. The machines travel with it.
That is why a bare Include "seq.shoddy" resolves in any folder
you open, with nothing set.
Optional — only if you've edited the extension: repackage from the repo root, which stages a fresh mill and freshly built machines into the package first:
./build.sh vsix # ./build.ps1 vsix on Windows
Optional — only if you're developing the
extension: to iterate without packaging at all, open the
vscode-shoddy/ folder in VS Code and press F5 — that launches an
Extension Development Host, a second VS Code window with your edited
extension loaded.
Tip If GitHub Copilot is in this
window too, spend five more minutes on
Grounding an Assistant. Copilot has never read
a line of Shoddy and will write you BASIC unless a
.github/copilot-instructions.md tells it otherwise; that page has
the file to copy.
The extension looks for a mill in four places, in order: the
shoddy.millPath setting; bin/mill inside your workspace
— the folder you have open (bin/mill.exe on Windows); its own
bundled copy; then mill on your PATH, the list of folders your
terminal searches for commands. The bundled copy means this section is
skippable for almost everyone.
The one ordering to know: a workspace's own bin/mill beats the
bundled one. That is deliberate. When the repo is your open folder, the mill
you just built is the one you want tested, not the one that shipped. Set
millPath explicitly in two cases: you keep several checkouts —
separate copies of the repository — and want the editor pinned to one
regardless of which is open, or something else named
mill shadows the real one.
If you do set it, set it in User
settings, not per-workspace. Then every folder you ever open — inside the
repo or out — finds it with zero per-folder setup:
Ctrl+Shift+P / Cmd+Shift+P →
Preferences: Open User Settings (JSON), then add:
"shoddy.millPath": "C:\\github\\shoddy\\bin\\mill.exe"
(macOS/Linux: the full absolute path to bin/mill — don't rely
on ~ expanding inside settings.)
The repo's .vscode/settings.json pins what matters either way:
Shoddy's blocks are indentation-based (a tab counts as 4) and the whole
corpus uses 4 spaces — let the editor keep it that way.
Start here: it's the fastest way to see the whole chain working, and it
needs zero configuration. Shoddy has native debugging — running your program
one line at a time while you watch what it does. In mill-speak the debugger
is the perch — where woven
cloth was inspected for flaws. Open any .shoddy file and press
F5: no launch.json needed. The program compiles with debug
instrumentation — the extra bookkeeping that lets the debugger watch each
line. It is the same program mill run builds, machines and
all, and it stops on its first line. If it would not compile, the launch fails
with the error the runner gives, rather than starting and dying part-way
through. Then:
.shoddy
file of your own. A machine is compiled code by the time your program runs,
so there is no line of it to stop on: a breakpoint set in
seq.shoddy comes back greyed out and says why. You step
over Sum and MatMul, the way you step over
any other runtime.F10), into (F11), out (Shift+F11).
Stepping moves one source line at a time. A tail-recursive def — one whose
last act is to call itself — loops in
place without growing the stack, exactly as it executes. Stepping into a
machine word steps over it: there is nothing of it left to step through.UPDATEBINV ← SOLVE ← MAIN.Person(Name = "ANN", Age = 34) and expand into fields), Globals
(top-level Lets), and the Value Stack — half the story in a
concatenative language.Limitation: under the perch, Input reads end-of-file, because
typed input (interactive stdin) isn't wired through the debug session. Debug
the logic here, and run the interactive session with the run task from
section 4.
Open the repo (code /path/to/shoddy) and the shipped
.vscode/tasks.json wires six
tasks to the mill — they're committed, so there's nothing to set up:
| Task | What it does |
|---|---|
| Shoddy: run current file | mill run on the file you're editing — bound to Cmd+Shift+B / Ctrl+Shift+B |
| Shoddy: weave current file | compiles to a .dll beside the source (dotnet FILE.dll runs it) |
| Shoddy: build machine from current file | compiles a machines/*.shoddy to Shoddy.Machines.<Name>.dll |
| Shoddy: show generated C# for current file | prints what the weave emits — the readable compiler output |
| Shoddy: golden conformance tests | dotnet test — the five goldens, the machines path, and the TCO check |
| Shoddy: rebuild mill into bin/ | re-publishes the mill into bin/ (folder) |
Workflow: open tst/examples.shoddy, press
Cmd+Shift+B / Ctrl+Shift+B, and the
program runs in the integrated terminal — the terminal panel inside the
editor. Everything else is under
Terminal → Run Task… (Cmd+Shift+P → “Run Task”).
Note the tasks are repo-local — in a folder outside the repo,
use the extension's own Γû╢ Run button (or ctrl+r), which works
everywhere.
Optional — only if you want a dedicated run key beyond
Ctrl+Shift+B: add to your keybindings.json
(Cmd+Shift+P → "Open Keyboard Shortcuts (JSON)"; on Windows use
ctrl+r ctrl+r):
{
"key": "cmd+r cmd+r",
"command": "workbench.action.tasks.runTask",
"args": "Shoddy: run current file",
"when": "editorLangId == shoddy"
}
Everything the mill builds can be built from in here — no terminal commands to remember:
FILE.dll, a self-contained
program that later runs anywhere .NET runs. (Running it is a command-line
affair — the Toolchain page — and one
exception applies: scribbler-window programs only run under
mill run, so weave console programs.)machines/*.shoddy and run Shoddy: build machine from
current file; it compiles to
Shoddy.Machines.<Name>.dll beside the source, which
run and weave link against automatically from then on.src/, run Shoddy: rebuild mill into bin/ to
re-publish the toolchain, then Shoddy: golden conformance
tests to prove it's still honest. Both are tasks; neither needs a
terminal.Nothing to configure here. This section explains what the extension does
with SHODDYLIB on your behalf, and matters only if you decide to
set one yourself.
The extension invokes mill five different ways — Run, Weave,
Machine, Show Generated C#, and F5 Debug — and they don't all receive
environment variables the same way. (An environment variable is a named
setting the operating system hands to a program when it starts.)
Run, Weave and Machine go through VS Code's integrated
terminal, which picks up terminal.integrated.env.* from
workspace settings. Show Generated C# and F5 Debug spawn the process
directly from the extension host — the background process that runs
extensions. Those two inherit only the
environment the VS Code process itself was launched with. Historically that
split was a trap: a SHODDYLIB set in workspace settings made Γû╢ Run
work while silently breaking F5 on the first Include-using program.
The extension now closes that gap itself. It points SHODDYLIB
at its own bundled machines by both routes at once — a terminal environment
collection for the terminal commands, and an explicit environment for the two
processes it spawns — so all five behave identically out of the box.
If you set SHODDYLIB yourself, the extension steps aside
entirely and yours wins everywhere. Set it where the VS Code process
will see it — a User environment variable on Windows, a shell-profile
export on macOS (VS Code resolves your login-shell environment at
launch, Dock launches included). Then restart VS Code fully
(macOS: Cmd+Q, relaunch), because a program copies its
environment variables once, at launch. Setting it in a workspace's
terminal.integrated.env reopens exactly the split described
above, so don't.
One other environment variable the mill reads: SHODDY_LINT,
set to anything, turns on the weave-time accessor-shadowing warning. The same
launch rules apply — set it where the VS Code process will see it if you want
F5 and Γû╢ Run to lint alike. What it warns about, and why it's off by default,
is on the Toolchain page.
Tasks run in the integrated terminal, so programs that use Input
just work — run tst/gradebook.shoddy with Cmd+Shift+B /
Ctrl+Shift+B and type at the prompts. To replay its scripted session
instead (a command-line affair — the only one on this page):
bin/mill run tst/gradebook.shoddy < tst/golden/gradebook.in
Do this once after installing the extension — and again whenever
SHODDYLIB or millPath changes, or a mill you build
yourself moves. Use a scratch folder outside any checkout. That is
the real test: it proves a program folder anywhere on disk will work, not just
files inside the source tree.
check.shoddy in a scratch folder:
Include "stats.shoddy"
Def Main()
Print(Sum({ 1, 2, 3 }))6, not unknown word: Sum or a “cannot
open” error. (Ctrl+Shift+B won't exist out here — the
tasks are repo-local.)Editor-side symptoms. If the command line is broken too, start with Setup's troubleshooting instead — fix the machine before the editor.
| Symptom | Likely cause |
|---|---|
| Works on Γû╢ (Run) but fails only on F5 or “Show Generated C#” | A SHODDYLIB of your own was set only in a workspace's
terminal.integrated.env, where the two spawned processes never
see it — section 5 |
unknown word: Sum (or any library word) on Run
and F5 alike |
A SHODDYLIB left over from an older install points somewhere
that no longer holds the machines — it overrides the bundled ones. Clear it
and fully restart VS Code (macOS: Cmd+Q); with it unset the
extension supplies its own |
macOS: works when VS Code is launched from a terminal
(code .) but not from the Dock |
Only possible with a SHODDYLIB or PATH of your
own: the login-shell environment wasn't resolved. Keep the
export in ~/.zshrc or ~/.zprofile and
fully relaunch — or drop it and let the bundled toolchain answer, which no
Dock launch can miss |
“The framework Microsoft.NETCore.App version 10 was
not found” |
The .NET 10 runtime isn't installed, or VS Code predates the install — Setup step A1, then restart VS Code |
| Mill not found at all | All four lookups missed, which normally means the package was built
without its mill staged. Check shoddy.millPath isn't set to a
stale path (it's checked first, and a wrong value beats the bundled copy),
then reinstall the .vsix from the Releases page |
Ctrl+Shift+B does nothing outside the repo |
Expected — the six tasks live in the repo's
.vscode/tasks.json. Use the Γû╢ Run button or
ctrl+r, which the extension provides everywhere |