Shoddy Documentation
Every message the mill can raise — the cause, and the fix, side by side
The mill prints three kinds of message on stderr (the terminal's error
channel). Weave errors stop the build before anything
runs. They look like ERROR (file.shoddy:N): message, and the
file is named because an Include can put the fault in another
file. Lint warnings never stop anything. They look like
file:N: warning: message, and --no-lint silences
them. Runtime errors stop the program at the failing
line: ERROR (line N): message. A bare
ERROR: message has no source line. The fault is in how the
program's pieces fit together, usually across machine DLLs (a DLL is a
compiled library file).
Every message is below, grouped by kind, each with broken code on the
left and the fix on the right. Placeholders like {name} stand
for whatever your program supplied.
Find your message: 0. Setup & environment | 1. Weave errors — layout & blocks · declarations · expressions & quotations · Select Case & patterns | 2. Lint warnings | 3. Runtime errors — types & stack · arithmetic · sequences · files, network & devices
A fourth kind of message comes first: the operating system and the .NET host, complaining before any Shoddy code is involved. These aren't Shoddy messages, but they're the ones a first day most often meets. The one-line summary: running Shoddy needs only the .NET 10 runtime. Building the toolchain from source needs the SDK. The VS Code extension carries everything else inside it (Setup).
You must install .NET to run this application.
The framework 'Microsoft.NETCore.App', version '10.0.0' was not found.
It was not possible to find any compatible framework version
The .NET 10 runtime is missing, or only an older one
(8, 9) is installed. Every path needs it: mill itself, a
woven dotnet x.dll (a program the mill has already built),
and the mill bundled inside the VS Code extension (where this message
appears in the extension's terminal). The message usually ends with the
right download link. Check what you have with:
$ dotnet --list-runtimes
Microsoft.NETCore.App 8.0.11 [...]
$ mill run hello.shoddy
You must install .NET to run this application.' install the .NET 10 RUNTIME (not the SDK) from
' dotnet.microsoft.com/download/dotnet/10.0
$ dotnet --list-runtimes
Microsoft.NETCore.App 10.0.x [...]
$ mill run hello.shoddy'dotnet' is not recognized as an internal or external command
dotnet: command not found
No .NET host is on the PATH at all — the list of folders your terminal searches when you type a command. Install the runtime (above). The installer sets the PATH for you. Then open a new terminal, because an already-open one keeps its old PATH.
'mill' is not recognized as an internal or external command
mill: command not found
mill: Permission denied
The mill isn't built, isn't on the PATH, or (on Unix) lost its execute
bit — the marker that tells Unix a file may be run as a program. A
.vsix is a zip, and the executable bit doesn't survive every
route through one. The extension restores it automatically. A
hand-unzipped copy may need it back.
$ mill run hello.shoddy
mill: command not found$ ./build.sh build # builds bin/mill
$ bin/mill run hello.shoddy # or add bin/ to PATH
$ chmod +x path/to/mill # Unix, if unzipped by handThe current .NET SDK does not support targeting .NET 10.0.
No .NET SDKs were found.
Building the toolchain from source — ./build.sh
build, dotnet test — needs the .NET 10
SDK, a bigger install than the runtime. Only contributors need
it. Writing and running Shoddy programs never does.
$ dotnet --list-sdks
(nothing, or only 8.x/9.x)
$ ./build.sh build
error NETSDK1045: The current .NET SDK does not
support targeting .NET 10.0.' install the .NET 10 SDK from
' dotnet.microsoft.com/download/dotnet/10.0
$ dotnet --list-sdks
10.0.xxx [...]
$ ./build.sh build… running scripts is disabled on this system.
Windows PowerShell is refusing the unsigned build.ps1.
Bypass the execution policy — Windows' rule about which scripts may
run — for this one invocation rather than changing it
system-wide.
PS> .\build.ps1 build
.\build.ps1 cannot be loaded because running
scripts is disabled on this system.PS> powershell -ExecutionPolicy Bypass -File .\build.ps1 buildcannot open '{path}' (for a bare machine include that should just work)
A stale SHODDYLIB. Bare includes resolve through
SHODDYLIB before the library beside the mill. So a
variable pointing at an old or moved checkout shadows the real machines
— it hides them behind its own copies. The VS Code extension manages
the variable for its own terminals. A shell you configured by hand keeps
whatever you set.
$ echo $SHODDYLIB
/old/checkout/machines # moved months ago
$ mill run game.shoddy
ERROR (game.shoddy:1): cannot open 'seq.shoddy'$ unset SHODDYLIB # let the mill's own
$ mill run game.shoddy # library resolve itunexpected indent
unexpected indent at top level (expected DEF at left margin)
A line is indented deeper than its context allows. The top level holds
Def, Type, Let, and
Include at the left margin. A body indents one step under its
header, and stays there.
Def Greet()
Print("HELLO")
Print("WORLD") ' deeper for no reasonDef Greet()
Print("HELLO")
Print("WORLD") ' one body, one depthexpected indented block after IF
expected indented block after ELSE
expected indented block after CASE
The header line parsed, but nothing indented follows it. Every
If … Then, Else, and Case needs at
least one line of body, indented one step.
Def Sign(n As Number) As String
If n < 0 Then
"NEGATIVE" ' body not indented past If
Else
"NOT NEGATIVE"Def Sign(n As Number) As String
If n < 0 Then
"NEGATIVE" ' one step deeper than If
Else
"NOT NEGATIVE"ELSE without matching IF
An Else sits at a depth where no If is open.
This is almost always an indentation slip: the Else is one
column off the If it belongs to.
Def Fee(age As Number) As Number
If age < 12 Then
5
Else ' deeper than its If
9Def Fee(age As Number) As Number
If age < 12 Then
5
Else ' same column as If
9IF must end its line
IF line must end with THEN
malformed IF condition
Statement-style If takes nothing after Then,
and nothing in place of it: the branch lives on the next line. For a choice
inside an expression, use Ifte(cond, a, b).
Def Cap(n As Number) As Number
If n > 10 Then 10 ' branch on the If line
Def Cap2(n As Number) As Number
If n > 10 ' Then missing
10Def Cap(n As Number) As Number
Ifte(n > 10, 10, n) ' inline choice
Def Cap2(n As Number) As Number
If n > 10 Then
10
Else
nDEF needs a name
expected DEF at top level
unexpected tokens after DEF name
A malformed Def header, or a statement sitting at the left
margin where only declarations may. The three header shapes:
Def Name(p As T, …) As T, Def Name(), and the
concatenative Def Name ( T … -- T ).
Def (n As Number) As Number ' no name
n * 2
Print("HI") ' a statement at top levelDef Twice(n As Number) As Number
n * 2
Def Main()
Print("HI") ' statements live in bodiesDEF {name} has an empty body (body must be indented)
TYPE {name} has no fields (fields must be indented)
The header parsed and then the next line was back at the left margin — the body or field list is missing, or not indented.
Type Point
X As Number ' fields at the margin
Y As Number
Def Origin() As Point
Point(0, 0) ' body at the marginType Point
X As Number
Y As Number
Def Origin() As Point
Point(0, 0)missing ) in DEF header
missing ) in variant
missing ) in pattern
expected parameter name, got '{tok}'
An opening parenthesis in the named construct never closed on its line.
Def Area(w As Number, h As Number As Number
w * hDef Area(w As Number, h As Number) As Number
w * hcannot open '{path}'
An Include whose file can't be found. Paths resolve relative
to the including file, then SHODDYLIB, then the machine
library beside the mill — so a bare machine name needs no path at all.
Include "machines/sqe.shoddy" ' typo, wrong pathInclude "seq.shoddy" ' bare name finds the libraryunterminated string literal
A string opened and the line ended before its closing quote. Strings
are single-line. A quote inside a string is written
\".
Print("HELLO) ' the quote never closesPrint("HELLO")
Print("SHE SAID \"HELLO\"")unclosed '{c}'
A bracket — (, {, [
— was still open at the end of the file. An open bracket is also how
a long line continues, so this often means a continuation you
didn't intend. The error points at the line that opened it.
Def Main()
Print(Sum({ 1, 2, 3 }) ' one ) short
Print("DONE") ' swallowed as continuationDef Main()
Print(Sum({ 1, 2, 3 }))
Print("DONE")'{word}': the characters ! % # $ ? are reserved and may not appear in words
Classic BASIC's type sigils are reserved — a sigil is a symbol on
the name that marks its type, like the $ in
name$. So is the predicate question mark (a ?
ending a word that answers true or false). Types live in declarations,
not names. Predicates take an Is prefix.
Let name$ = "ADA"
Def Empty?(xs As List Of t) As Boolean
IsEmpty(xs)Let name = "ADA"
Def IsBlank(xs As List Of t) As Boolean
IsEmpty(xs)'{word}': '{char}' is an operator and must be spaced away from the name, as in 'count + 1'
Words split on whitespace and nothing else, so count+1 is
one long name rather than an addition. Without this check it would survive
lexing (the mill's first pass, which cuts the file into words). It would
then die much later as an undefined word — a message that says
nothing about the missing space. Put spaces around the operator. The
operators themselves are unaffected, and so are number literals:
1E+10 is still a number.
Def Next(count As Number) As Number
If count>0 Then
count+1
Else
0Def Next(count As Number) As Number
If count > 0 Then
count + 1
Else
0'{word}': the characters ; \ are reserved and may not appear in words
Neither character spells anything in Shoddy. Layout ends a statement,
so there are no semicolons. \ is only a string escape (a
marker that gives the next character a special meaning). Inside a string
literal it keeps that meaning — this rule is about words.
Def Main()
Print("HELLO");
Print(C:\notes.txt)Def Main()
Print("HELLO")
Print("C:\\notes.txt")expected INCLUDE "FILE" or INCLUDE "FILE" AS NAMESPACE …
'{tok}' is not usable as a namespace — …
'{file}' is included twice under different namespaces …
The two Include shapes: a quoted filename alone, or the
filename followed by As Name, where the name is a plain word.
One file gets at most one namespace (the prefix that keeps its names apart
from yours), however many routes reach it.
Include seq.shoddy ' quotes required
Include "vt100.shoddy" As "T" ' namespace is a word
Include "util.shoddy" As A
Include "util.shoddy" As B ' same file, two namesInclude "seq.shoddy"
Include "vt100.shoddy" As T
Include "util.shoddy" As A ' one file, one nameIN wants a plain name on each side, as in 'CaveNearby In Msg'
In picks a namespaced meaning for a bare word. Write
Name In Namespace, both sides plain words. The pair collapses
to the single word NamespaceName.
Print(Camp() In N) ' a call is not a plain namePrint(Camp In N) ' resolves, then callsduplicate definition of {name} — …
Two top-level declarations claim one case-folded name. Case-folded
means capital and small letters count as the same letter:
total, Total, and TOTAL are the
same word. The message names the earlier site, which may be in an
Included file. Rename one, or namespace an include with
As.
Def Total(xs As List Of Number) As Number
Fold(xs, 0, +)
Let total = 100 ' same word, case-foldedDef Total(xs As List Of Number) As Number
Fold(xs, 0, +)
Let startingTotal = 100{name} is a builtin — a Def of that name would …
A builtin — a word the language itself supplies — can't be
redefined by accident. Pick another name. If the shadowing is deliberate,
declare it with Redef, which waives the check on purpose.
Def Str(n As Number) As String
"#" & "?" ' STR is the builtinDef Tagged(n As Number) As String
"#" & Str(n)duplicate definition of {name} across machines — include one of them under a namespace (AS)
Two machine DLLs export the same word and both were included bare. Give one a namespace. Its words then answer to the prefixed name.
Include "net.shoddy"
Include "turtle.shoddy" ' both export CLOSEInclude "net.shoddy"
Include "turtle.shoddy" As T
' net's Close, and CLOSE In T (TCLOSE)machine {name} declares dependency {dep}, but its DLL could not be found beside {path} or its machine library
machine {name} references {dep}, but its DLL could not be found beside {path} or its machine library
'{path}' is not a Shoddy machine (no manifest)
A machine DLL needs a sibling DLL that isn't beside it, or a stray non-machine DLL sits where a machine was expected. This is a build-state problem, not a code problem. Rebuild the library:
# machines/bin holds Isam.dll but
# someone deleted Seq.dll beside it./build.sh machines # Unix
./build.ps1 machines # Windowsno DEF MAIN found
A program run with mill run must define Main.
A library built with mill machine must not — it is included,
not run.
' mill run lib.shoddy
Def Capped(n As Number) As Number
Min(n, 10)' either add an entry point...
Def Main()
Print(Capped(99))
' ...or build it as what it is:
' mill machine lib.shoddya machine's top-level Let must bind a constant; this one calls {word}
a machine's top-level Let cannot hold a quotation; return it from a Def instead
a machine's top-level Let cannot hold a conditional; And and Or compile to one, so neither folds
the constant '{name}' cannot be computed: {why}
A machine has no Main, so it has no init phase to run an
initializer in. A constant needs none: it is built once when the
machine loads. That is why a machine may carry a constant and nothing
else. Literals, { … } list literals and type
constructors are constants. So are + - * / ^ Mod & over
them, and ToArray/Dim and
True/False, all folded where they are written
(folded: computed at build time, with the answer stored). Those four are
folded for one reason: the language has no array literal and no boolean
literal, so the word is the only spelling either value has. Anything else
that calls a word is refused, and the message names the word rather than
the feature.
The third message is the fold reporting what it found. A constant division by zero is refused at compile time, and the refusal names the binding — instead of aborting in the middle of somebody's run.
A quotation — a function value — gets its own message
because it is not a matter of taste. A woven quotation body is a closure
over the running Engine: a function that carries the Engine it was made
inside. A value built when the machine loads has no Engine to close over.
So a function-valued constant cannot be emitted at all. Keep it in a
Def, which is per-Engine and correct.
A conditional gets its own message for the same reason a quotation
does: the word it would otherwise name is one you never wrote.
And and Or short-circuit — the right side
runs only if the left has not already settled the answer — so the
parser compiles them to a conditional. Reporting
Let ok = True And False as calls IF would send you
looking for an If that is not in the file. Write the answer,
which is a constant either way, or compute it in a Def.
' mill machine units.shoddy
Let names = Map(Raw(), Upper) ' calls MAP
Let pick = Fn(x) => x + 1 ' a quotation
Let half = 1 / 0 ' cannot be computedLet names = { "m", "kg", "s" } ' a constant...
Def Names() As List Of String ' ...published by a Def
names ' Names() = Names() is True
Def Pick() As Quotation ' computation stays in a Def
Fn(x) => x + 1expected TYPE NAME or TYPE NAME = VARIANT | ...
duplicate field {name}
expected field name, got '{tok}'
expected AS after field name
A malformed record. The shape: Type Name on its own line,
then one Field As Type per indented line, each field named
once.
Type Student
Name As String
Name As Number ' duplicate field
Score ' As Type missingType Student
Name As String
Score As Numberexpected variant name, got '{tok}'
expected | between variants
trailing | in sum type
sum type needs at least one variant
A malformed sum type. The shape is all on the header line:
Type T = A | B(X) | C — variants separated by |,
none trailing, a bare variant meaning zero fields.
Type Coin = Heads Tails | ' missing |, then trailingType Coin = Heads | Tailsunknown word: {name}
Nothing declares the word — not a local, Let,
Def, machine word, constructor, builtin, or field accessor. A
typo, or a missing Include. Raised at weave time by the linter,
and no flag overrides it: the program would die at the same spot at
runtime.
Def Main()
Print(Lenght("HELLO")) ' typo
Print(Trim(" HI ")) ' str never includedInclude "str.shoddy"
Def Main()
Print(Len("HELLO"))
Print(Trim(" HI "))unknown word: {name} — declared in {file}, which {other} includes but does not export. Add: Include "{file}"
The same refusal, with the answer attached. A machine exports the words
it declares. What it includes is its own business: including
stats.shoddy does not hand you seq's words, even
though stats uses them itself. Add the Include the message names. The
dependency is already compiled and linked, so nothing else changes.
Include "stats.shoddy"
Def Main()
Print(Mean(ZipWith({ 1, 2 }, { 3, 4 }, +))) ' ZipWith is seq'sInclude "seq.shoddy"
Include "stats.shoddy"
Def Main()
Print(Mean(ZipWith({ 1, 2 }, { 3, 4 }, +)))malformed CASE — {name} is declared in {file}, which {other} includes but does not export. Add: Include "{file}"
The same rule, met through a pattern rather than a call. A name that
does not resolve to a type is not a pattern at all. So a Case
on a constructor you cannot reach fails as syntax, and the message says
which Include would make it one. Pair is the usual case: it
is seq's, and half the library hands one back.
Include "dict.shoddy"
Def Fst(p As Pair) As Number
Select Case p
Case Pair(a, b)
aInclude "seq.shoddy"
Include "dict.shoddy"
Def Fst(p As Pair) As Number
Select Case p
Case Pair(a, b)
a'{name}' is ambiguous — it names {A} and {B}; write '{name} In <namespace>' to choose
Two namespaces both declare the bare word, so the mill refuses to pick one silently. Qualify the use site.
Include "north.shoddy" As N
Include "south.shoddy" As S
' both declare Def Camp()
Def Main()
Print(Camp())Include "north.shoddy" As N
Include "south.shoddy" As S
Def Main()
Print(Camp In N){Type} expects {n} fields, got {m}
A constructor call with the wrong number of arguments. Fields are
positional and all required — check the Type declaration's
order.
Type Student
Name As String
Score As Number
Def Main()
Print(Student("ADA")) ' Score missingType Student
Name As String
Score As Number
Def Main()
Print(Student("ADA", 96)){Type} has no field {name}
{Type}: missing field {name}
WITH needs at least one FIELD = value
WITH expects FIELD = value, got '{tok}'
A With(record, Field = value) naming a field the type
doesn't have, or naming none, or malformed. With returns a new
record with the named fields replaced.
Type Student
Name As String
Score As Number
Def Bump(s As Student) As Student
With(s, Points = 100) ' no such fieldType Student
Name As String
Score As Number
Def Bump(s As Student) As Student
With(s, Score = Score(s) + 1)unexpected '{tok}' in expression
unexpected end of line in expression
The expression parser hit something it can't place — a missing operand, a stray comma, or a line that ended mid-thought. To wrap a long expression, put it in parentheses: an open bracket continues the line.
Def Main()
Print(1 + ) ' operand missing
Let x = 2 + ' line ends mid-expression
3Def Main()
Print(1 + 2)
Let x = (2 +
3) ' parentheses carry the wrapmissing ] (quotations may not span lines)
] without matching [
{tok} is not allowed inside [ ] — use IFTE for inline conditionals
A quotation literal [ … ] closes on its own line and holds
words and literals only — no If, no statements. For logic
inside a function value, use Ifte or Fn.
Def Main()
Print(Map({ 1, 2 }, [ Dup If ]))Def Main()
Print(Map({ 1, 2 }, Fn(n) => Ifte(n > 1, n, 0)))expected LET NAME = expression
expected Let NAME = expression
Let expects a name, got '{tok}'
unexpected tokens after LET expression
unexpected tokens after Let expression
Malformed Let. Exactly Let name = expression,
one binding per line, and the binding is final — Let never
rebinds.
Def Main()
Let 2x = 10 ' names start with a letter
Let y = 1 Let z = 2 ' one per lineDef Main()
Let doubled = 10
Let y = 1
Let z = 2TAKE belongs to concatenative bodies; use LET or named parameters here
TAKE needs at least one name
TAKE expects names, got '{tok}'
FN expects parameter names, got '{tok}'
Take pops stack values by name and lives only in
stack-style bodies. Call-style code binds with parameters and
Let. Inline functions bind with
Fn(name, …) =>.
Def Twice(n As Number) As Number
Take m ' Take in a call-style body
m * 2Def Twice(n As Number) As Number
n * 2
Def Twice2 ( Number -- Number )
Take n ' Take at home, stack-style
n * 2expected SELECT CASE expression
malformed SELECT CASE expression
SELECT CASE needs at least one CASE
expected indented CASE clauses after SELECT CASE
expected CASE inside SELECT CASE
malformed CASE
The Select Case grammar: a scrutinee (the value being
tested) on the header line, then indented Case clauses
— values, any-of lists, ranges, comparisons — each with its
own indented body.
Def Grade(s As Number) As String
Select Case ' scrutinee missing
Case Is >= 90
"A"
"B" ' body without a CaseDef Grade(s As Number) As String
Select Case s
Case Is >= 90
"A"
Case 70 To 89
"B"
Case Else
"F"CASE after CASE ELSE
ELSE must be alone on its line
Case Else is the default and comes last; nothing follows
it, and Else shares its line with nothing.
Select Case n
Case Else
0
Case 1 ' after the default
1 Select Case n
Case 1
1
Case Else
0incomplete pattern
pattern expects names or TYPE(...), got '{tok}'
unknown type '{tok}' in pattern
{Type} has {n} fields but the pattern names {m}
malformed CASE pattern
A destructuring Case — one that opens a record and
names its fields — must name a declared type and bind every field.
Use _ for the ones you don't need. Patterns nest.
Type Student
Name As String
Score As Number
Def Show(s As Student) As String
Select Case s
Case Student(n) ' two fields, one name
nType Student
Name As String
Score As Number
Def Show(s As Student) As String
Select Case s
Case Student(n, _) ' _ ignores Score
nDoctrine and flags live in The linter:
warnings go to stderr, never stop a build, and fire only on what the
checker can prove. Silence them with --no-lint. See coverage
with --lint-verbose.
Include "{file}" — no word or type from it is used
An include that earns nothing. A machine exports only what it declares,
so an Include is a statement about what this file uses rather
than an incidental splice. An Include contributing no name is noise. It is
also how a re-export habit starts: a machine that includes something
purely so its callers get it too. Delete the line, or use the machine.
Include "seq.shoddy"
Include "money.shoddy" ' nothing below names a money word
Def Main()
Print(Length(Taken({ 1, 2, 3 }, 2)))Include "seq.shoddy"
Def Main()
Print(Length(Taken({ 1, 2, 3 }, 2)))'{name}' shadows field accessor '{Field}' of type '{Type}' — the accessor is unreachable in this scope
Def '{name}' shadows field accessor '{Field}' of type '{Type}' — the accessor is unreachable
A mid-body Let (or a whole Def) reuses the
name of a field this program reads by accessor — the word, named
after the field, that reads it out of a record. Inside that scope the
accessor silently stops working. The program computes a wrong answer, not
an error. Rename the binding. Parameters named after fields are
deliberately allowed. Only Lets warn.
Type Census
Aloft As Number
Landed As Number
Def Report(c As Census) As Number
Let landed = 99 ' shadows the accessor
Landed(c) + landed ' Landed no longer reads cType Census
Aloft As Number
Landed As Number
Def Report(c As Census) As Number
Let bonus = 99
Landed(c) + bonusDef '{name}' ({site}) collides with field accessor '{Field}' of type '{Type}' — the word wins and the accessor is unreachable; rename one of them
machine word '{name}' … collides with field accessor …
A word and a record field share one case-folded name, and the program
writes that word somewhere. Every mention resolves to the word, never the
field. Rename one side. (str's decimal formatter is spelled
ToFixed for exactly this reason: mungo owns a
Fixed field.)
Type Crate
Weight As Number
Sealed As Boolean
Def Sealed(c As Crate) As String ' the word wins
"STAMPED"Type Crate
Weight As Number
Sealed As Boolean
Def SealStamp(c As Crate) As String
Ifte(Sealed(c), "STAMPED", "OPEN")'{name}' is written as a call, but here '{name}' is a local shadowing the builtin — a local cannot be called, so its value passes through unchanged; rename the local
… shadowing the machine word …
… shadowing the Def …
A parameter or Let shares its case-insensitive name with
the word the call means to reach. A local wins every naming contest, so
the call applies nothing and the value slides through untouched. The
classic symptom is a list walker that never advances:
Rest(rest) answers rest unchanged, and the
program hangs rather than erring. Rename the local. Under
--lint-verbose the linter also notes every dormant
shadow of this kind: note: parameter '{name}' shadows the builtin
'{name}' — the local wins every naming contest in this scope; rename
it. That note is quiet by default for a reason. Parameters named
rest, args or mid are the recursion
idiom across the library, and a dormant shadow costs nothing until
called.
Def Walk(rest As List Of Number) As Number
If IsEmpty(rest) Then
0
Else
1 + Walk(Rest(rest)) ' the local wins: never advancesDef Walk(more As List Of Number) As Number
If IsEmpty(more) Then
0
Else
1 + Walk(Rest(more))'{name}' takes {n} value(s), but this call writes {m} argument(s) — the extra value(s) land beneath the call and surface somewhere else
The call writes more arguments than the word consumes, usually because a misplaced paren folds two arguments into one call. It still compiles. The callee binds whichever values sit on top, and the extras surface as a type error or a wrong answer far from this line. Writing fewer arguments than the word takes is deliberately not judged: feeding the rest from the stack is the point-free pipeline idiom (calling a word without spelling out all of its inputs).
Def Inc(n As Number) As Number
n + 1
Def Main()
Print(Inc(1, 2)) ' one param, two argumentsDef Inc(n As Number) As Number
n + 1
Def Main()
Print(Inc(1) + 2)Def '{name}' leaves {n} values on the stack — a Def yields one value, or none
The body provably pushes more than one value. The usual cause is an
expression on its own line that was meant to combine with the next. Every
path through a Def must net exactly one value (a value Def)
or zero (a void one).
Def Total(a As Number, b As Number) As Number
a
b ' two values left behindDef Total(a As Number, b As Number) As Number
a + b ' one value outDef '{name}' reaches {n} value(s) below its own parameters — something consumed more than it was given
Something in the body eats more values than the parameters supplied. Typically a word silently resolved to something with a different arity — a different number of inputs — than you meant: an accessor where a local was intended, or a two-argument word called with one.
Def Third ( Number Number -- Number )
Drop
Drop
Drop ' three drops, two values givenDef Third ( Number Number Number -- Number )
Drop
Drop
Drop ' the signature now supplies threethe branches of this IF disagree — one nets {n} value(s), the other {m}; every branch must leave the same number of values
One arm yields a value and the other doesn't. Both arms of an
expression If must produce. Both arms of a void one must not.
The classic cause is a void base case — the branch that ends a
recursion — "padded" with a spare call.
Def Fee(vip As Boolean) As Number
If vip Then
0
Else
Print("FULL PRICE") ' prints, yields nothingDef Fee(vip As Boolean) As Number
If vip Then
0
Else
9a quotation reaches '{op}', which needs a number — an operator called function-style is a section; write `a Op b` (infix) or parenthesize the operand
IF's condition is a quotation — …
IFTE's condition is a quotation — …
Calling an operator like a function — Mod(a, 2)
— is legal syntax, but it builds an operator section (a
function value), not a result. So does a unary minus before parentheses.
The value then reaches a slot that needed a number or boolean. Write the
operator infix: between its two operands.
Def IsOdd(n As Number) As Boolean
Mod(n, 2) = 1 ' a section, not a number
Def Wave(z As Number) As Number
Sin(-(z * z)) ' section of binary minusDef IsOdd(n As Number) As Boolean
n Mod 2 = 1 ' infix
Def Wave(z As Number) As Number
Sin(0 - z * z) ' subtract from zerobare name '{name}' was passed as a function value, but nothing defines it — a typo is auto-quoted silently in argument position
A bare name in argument position is passed as a function, not called.
That is how Map(xs, Square) works. It also means a typo there
becomes a quotation of nothing instead of an error. Check the spelling.
Def Double(n As Number) As Number
n * 2
Def Main()
Print(Map({ 1, 2 }, Duoble)) ' typo, quotedDef Double(n As Number) As Number
n * 2
Def Main()
Print(Map({ 1, 2 }, Double))this line begins with '{And/Or}' — a wrapped condition must be parenthesized to continue the previous line
Lines continue only inside open brackets. A condition wrapped without them parses as a new statement whose infix grabs a stray stack value. The wrong answer then surfaces far away. Parenthesize the whole condition.
Def Ok(a As Number, b As Number) As Boolean
a > 0
And b > 0 ' a fresh statement, not a wrapDef Ok(a As Number, b As Number) As Boolean
(a > 0
And b > 0) ' brackets carry the wrap'{callee}' matches this parameter against sum constructors, but a number/string/boolean can never match one — this call always falls to Case Else
The callee Select Cases that parameter over a sum type. A
literal can never equal a constructor, so the dispatch silently takes
Case Else — no error, just the wrong arm, forever. Pass the
constructor.
Type Speed = Slow | Fast
Def Limit(s As Speed) As Number
Select Case s
Case Slow
30
Case Else
70
Def Main()
Print(Limit(1)) ' always 70Type Speed = Slow | Fast
Def Limit(s As Speed) As Number
Select Case s
Case Slow
30
Case Else
70
Def Main()
Print(Limit(Slow())) ' 30top-level Let '{name}' in a namespaced include shares its name with another namespace — resolution here is known to misfire; move the value into a Def
File-scope Lets under Include … As resolve
unreliably when another namespace claims the same bare name. A
Def holding the value is immune.
The warning is narrower than the hazard, so read it as the tip
of one. A top-level Let in a file included
As Q is declared under Q. A bare
reference to it from another file in argument position is
auto-quoted into a quotation rather than resolving to the value. There is
no clash and no warning, and the value arrives as a function nobody
called:
' lib.shoddy, included As Q, holding: Let answer = 42
Print(LibPeek()) ' 42 — read inside its own file
Print(Str(answer)) ' STR expects a NUMBER, got QUOTATION
Until that is fixed, a file anyone includes under a namespace should keep
its constants in Defs. mungo-caverns does exactly
that, and says so at the include block that creates its namespaces.
' colors.shoddy — included As C
Let limit = 16
' sizes.shoddy — included As S
Let limit = 100' colors.shoddy — included As C
Def Limit() As Number
16
' sizes.shoddy — included As S
Def Limit() As Number
100{who} expects a NUMBER, got {type}
{who} expects a STRING, got {type}
{who} expects a BOOLEAN, got {type} (booleans are not numbers in Shoddy)
{who} expects a LIST or ARRAY, got {type}
{who} expects a QUOTATION, got {type}
{who} expects a QUOTATION or LIST, got {type}
{who} expects a RECORD, got {type}
{who} expects a SCRIBBLER, got {type}
The named word popped the wrong kind of value. Two causes account for
almost all of them. First, a number used where a boolean belongs: they are
distinct types in Shoddy, and If n Then is not "if nonzero".
Second, a quotation arriving where a number belongs. That usually means an
operator was called function-style — see the section warning
above.
Def Main()
Let n = 3
If n Then ' a number is not a boolean
Print("YES")Def Main()
Let n = 3
If n <> 0 Then ' say the comparison
Print("YES")stack underflow
A word needed more values than the stack held — in stack-style code, an arity slip. (In call-style code the linter's depth checks usually say this earlier, with a better message.)
Def Sum2 ( Number Number -- Number )
+ + ' second + has one value, needs twoDef Sum2 ( Number Number -- Number )
+unknown word: {name} (at runtime)
The runtime backstop for the weave-time error of the same name. Reaching it at runtime means the program was woven before the linter existed — rebuild it and the weave will point at the site instead.
{Type} has no field {name}
WITH expects a RECORD, got {type}
A field read, or a With, met a value of the wrong record
type — usually the wrong record reached the site, not the wrong field
name.
Type Point
X As Number
Y As Number
Type Size
W As Number
H As Number
Def Main()
Let s = Size(3, 4)
Print(X(s)) ' a Size has no XType Point
X As Number
Y As Number
Type Size
W As Number
H As Number
Def Main()
Let s = Size(3, 4)
Print(W(s))ASSERTION FAILED: {msg}
SELECT CASE: no matching CASE
… and any text your own Error("…") prints
One of three things happened: Assert(cond, "MSG") failed;
a Select Case with no Case Else met a value no
arm matched; or the program called Error itself. The library
speaks through the same channel —
SPLIT: EMPTY SEPARATOR, TOBOOL: '{s}' is not TRUE or
FALSE, MPS: NO OBJECTIVE (N) ROW,
BOOLBITAND: '-1' IS NEGATIVE - THE DOMAIN IS WHOLE NUMBERS FROM 0,
BOOLBITNOT: WIDTH '54' IS OUTSIDE 1..53 — each
documented on its machine's page.
bool carries the fullest list. Its
whole point is refusing a bad value rather than masking one, so it has the
most to refuse.
Def Name(k As Number) As String
Select Case k
Case 1
"ONE"
Case 2
"TWO"
' Name(3) aborts: no matching CASEDef Name(k As Number) As String
Select Case k
Case 1
"ONE"
Case 2
"TWO"
Case Else
"MANY"division by zero MOD by zero
WRAP by zero invalid exponentiation
SQR of negative number
LOG of non-positive number LOG10 of non-positive number
ASIN outside [-1, 1] ACOS outside [-1, 1]
GAMMAP: A must be positive GAMMAP: X must be non-negative
BETAI: A and B must be positive BETAI: X outside [0, 1]
Out-of-domain mathematics: the input is outside the set of values the
function can answer for. Guard the argument — test it — before
the call. Division and Mod by zero, roots of negatives, and
logs of zero are questions with no numeric answer. Shoddy stops rather
than inventing one.
Def Share(total As Number, people As Number) As Number
total / people ' people may be 0Def Share(total As Number, people As Number) As Number
If people = 0 Then
0
Else
total / peopleVAL: '{s}' is not a number
Val stops the program when the text isn't a number. It
never guesses zero. That strictness is by design. Guard with
IsNumeric, or use ValOr(s, fallback) when a
default is the right answer.
Def Main()
Let age = Val(Input("AGE? ")) ' "ten" abortsDef AskAge() As Number
Let s = Input("AGE? ")
If IsNumeric(s) Then
Val(s)
Else
AskAge() ' re-ask until it parsesFIRST of empty sequence
REST of empty list
Check for the empty list first. Return the base answer for it. Only
then take First and Rest. Guard with
IsEmpty: the base case goes before the destructuring, and
recursion's shape follows.
Def Sum(xs As List Of Number) As Number
First(xs) + Sum(Rest(xs)) ' no floorDef Sum(xs As List Of Number) As Number
If IsEmpty(xs) Then
0
Else
First(xs) + Sum(Rest(xs))NTH: index {k} out of range 1..{n}
SETNTH: index {k} out of range 1..{n}
DIM: negative size
Shoddy indexes from 1, inclusive at both ends — the classic off-by-one is
asking for index 0, or for Length(xs) + 1.
Def Main()
Let xs = { "A", "B", "C" }
Print(Nth(xs, 0)) ' there is no zerothDef Main()
Let xs = { "A", "B", "C" }
Print(Nth(xs, 1)) ' first is 1
Print(Nth(xs, Length(xs))) ' last is LengthMAP quotation must leave exactly one value
FILTER quotation must leave exactly one value
The per-element function must yield exactly one value —
Print yields none, a stray pair yields two.
Def Main()
Print(Map({ 1, 2 }, Fn(n) => Print(n)))Def Main()
Each({ 1, 2 }, Fn(n) => Print(n)) ' effects: Each
Print(Map({ 1, 2 }, Fn(n) => n * 2))list element must yield exactly one value
{who}: each list item must yield exactly one value
Each element of a list literal is an expression that must produce
exactly one value — a Print produces none.
Def Main()
Let xs = { 1, Print("TWO"), 3 }Def Main()
Let xs = { 1, 2, 3 }
Print("TWO")SORT expects all NUMBERs or all STRINGs
{op} expects two NUMBERs or two STRINGs
CONCAT expects two LISTs or two ARRAYs
ASC of empty string
Mixed or mismatched operands — comparisons (<,
>, <=, >=) raise the same
complaint when the two sides differ in kind. Sort one kind at a time,
compare like with like, and give Asc at least one
character.
Def Main()
Print(Sort({ 3, "TWO", 1 }))Def Main()
Print(Sort({ 3, 2, 1 }))
Print(Sort({ "C", "B", "A" }))CODEAT: position {k} is past the end of a {n}-character string
CODEAT: position must be a whole number from 1
The program read a character that is not there. This is the message
Asc(Mid(s, k, 1)) will not give you. Mid
clamps: a start past the end quietly becomes the empty string.
Asc then aborts naming itself. So the report blames
Asc, while the mistake is in whatever computed k
— possibly several Defs away, and only on inputs long enough to
reach the end. CodeAt refuses at the index instead. It names
both the position and the length, so the off-by-one is visible. The two
bounds are separate messages deliberately: below 1 is a counting mistake,
past the end is a length mistake. A caller that wants neither checks
against Len first, or converts once with Codes
and walks the numbers.
Def Main()
Let s = "HELLO"
Print(Asc(Mid(s, 6, 1))) ' aborts naming ASC, not the 6Def Main()
Let s = "HELLO"
Print(CodeAt(s, 5)) ' 79 — and 6 would name the index
Print(Nth(Codes(s), 5)) ' the same, when walking the whole stringFROMCODES: code {c} is outside 0 to 65535
FROMCODES: code {c} is not a whole number
INSTRFROM: start must be a whole number from 1
A code that is not a character, or a position to search from that is
not a position. FromCodes refuses the same range
Chr does, and for the same reason: the cast (the forced
conversion) to a 16-bit character wraps silently. 65536 would answer some
other character rather than failing. It refuses a fraction for the same
reason again: the cast would truncate it. InstrFrom counts
from 1 like every other position in the language. Note what is
not an error: a start past the end of the subject answers
0, because nothing occurs there and that is an answer.
Def Main()
Print(FromCodes(ToArray({ 72, 65536 })))
Print(InstrFrom("HELLO", "L", 0))Def Main()
Print(FromCodes(ToArray({ 72, 73 }))) ' HI
Print(InstrFrom("HELLO", "L", 1)) ' 3
Print(InstrFrom("HELLO", "L", 9)) ' 0, not an errorREADFILE: cannot open '{path}'
BOPEN: cannot open '{path}'
DELETEFILE: cannot delete '{path}'
BOPEN: too many open files
Path, permission, or a leak — binary files stay open until
BClose. Guard existence with FileExists when
absence is an ordinary case.
Def Main()
Print(ReadFile("scores.txt")) ' may not existDef Main()
If FileExists("scores.txt") Then
Print(ReadFile("scores.txt"))
Else
Print("NO SCORES YET"){WORD}: '{path}' is outside the file root
Only ever seen when a host is running the mill. An application
that embeds Shoddy gives it a root directory, and no file word may read,
write, delete or open outside it. A path that climbs out with
.., an absolute path elsewhere, or a symbolic link pointing
away (a file that is really a signpost to another path) are all refused
before anything is touched. Relative paths resolve against that root, so
plain names are what you want. mill run sets no root and is
not affected: this cannot happen at a terminal.
The guarded twins report it instead of stopping —
TryWriteFile and TryDeleteFile answer
False, TryReadFile and TryBOpen
answer Err — and FileExists answers
False, so nothing outside can be probed for.
Def Main()
WriteFile("../../notes.txt", "hello")
' ERROR: WRITEFILE: '../../notes.txt' is outside the file rootDef Main()
WriteFile("notes.txt", "hello") ' in the root
WriteFile("saved/notes.txt", "hello") ' and below itGETNUM: read past end of file
GETBOOL: read past end of file
GETSTR: read past end of file
SEEK: position must be >= 1
GETSTR: field length must be >= 1
PUTSTR: field length must be >= 1
PUTSTR: string of {n} bytes exceeds the {len}-byte field
Binary record I/O is exact: fixed field widths, 1-based positions, and
the file ends where it ends. Let recio do
the offset arithmetic — RecSeek, GetRec,
RecCount — instead of hand-rolled seeks.
Def ReadAll(f As Number)
Print(GetNum(f))
ReadAll(f) ' reads past the endDef ReadAll(f As Number, k As Number, n As Number)
If k <= n Then
Print(GetNum(f))
ReadAll(f, k + 1, n)
' count records first: RecCount(f, size){word}: bad file handle
{word}: bad socket handle
{word}: bad scribbler slot
A number reached a file or socket word that no BOpen /
TcpConnect ever returned — or one that was already
closed. A handle is the number that names an open file or connection.
Thread it from open to close, and close exactly once.
The scribbler one is the same fault in a place most programs never go.
TryScribblerOpen answers a slot number rather than a
window, so that a reckoner seed can keep a window in its named resource
table. An ordinary program takes the window itself from
ScribblerOpen and never sees a slot. Reaching
ScribblerOf or a slot-taking word with a number that is not
open means the thing holding the binding lost track of it.
Def Main()
Let f = BOpen("data.bin")
BClose(f)
Print(GetNum(f)) ' used after closeDef Main()
Let f = BOpen("data.bin")
Print(GetNum(f))
BClose(f) ' close lastInput: cannot read the console while a scribbler window is open — read keystrokes with ScribblerWait or ScribblerPoll.
InputLine: cannot read the console while a scribbler window is open …
InKey: cannot read the console while a scribbler window is open …
The console and a scribbler window are separate input channels, routed by which one the operating system gives focus (the keyboard's attention). A program with a window open reads keys from the window's events, not the console prompt. The refusal applies only to the live console. Redirected or host-piped input has no focus to lose, so a piped program may keep a window open beside a prompt and read both.
Def Loop(sc As Scribbler) As Scribbler
Let cmd = Input("> ") ' window is openDef Loop(sc As Scribbler) As Scribbler
Select Case NextEvent(sc)
Case ScribblerKeyDown(key, mods, at)
Loop(OnKey(sc, key))
Case Else
Loop(sc){word}: network is disabled — run the mill with --allow-net
The TCP builtins (the words that open network connections) are a gated capability, off by default. The fix is on the command line, not in the code.
mill run chat.shoddymill run --allow-net chat.shoddyTCPCONNECT: '{host}:{port}' timed out
TCPCONNECT: cannot reach '{host}:{port}'
TCPLISTEN: cannot bind {host}:{port}
TCPLISTEN: '{host}' is not an IP address (try "127.0.0.1")
TCPSEND: send timed out
TCPRECV: byte count must be >= 1
TCPSEND / TCPRECV: {socket error}
TCPACCEPT: too many open sockets
Ordinary network trouble, reported with the peer (the machine at the
other end) and the port: the far side isn't listening, the port is taken,
the hostname needed to be an address, or sockets leaked without
TcpClose.
TCPSECURE: handshake with '{host}' failed — {reason}
TCPSECURE: already secured
TCPSECURE: handle is a listener
TcpSecure upgrades a connected socket to TLS, the
encryption layer behind https. The host you pass drives both the SNI
extension (the name sent to the server during the handshake) and
certificate name validation. So it must be the name the certificate was
issued for. An IP address will not match one. A failed handshake takes the
handle with it: the socket is closed and its slot cleared. A half-open
connection is no use to a language with no catchable errors. The reason
comes from the platform. Certificate is not trusted is the usual
one, seen when a server presents a self-signed certificate (one it vouches
for itself).
Only a connected socket can be secured. A listener cannot, and server-side TLS is not supported. Securing twice is refused rather than silently layering a second stream on the first.
Let s = Connect("93.184.216.34", 443)
Secure(s, "93.184.216.34") ' an IP is not a certificate nameLet s = Connect("example.com", 443)
Secure(s, "example.com")TCPPOLL: not meaningful on a secured connection
TCPRECV: secure read timed out
A secured socket reads blocking — the read waits
until there is data — and that is deliberate. Once TLS records wrap
the stream, a TCP-level poll stops telling the truth about plaintext (the
decrypted data). A partial record makes bytes look ready that decrypt to
nothing yet. A fully buffered record makes plaintext look absent when a
read would return it at once. Rather than answer dishonestly,
TcpPoll refuses — and so does Ready, which
is built on it.
Read the socket instead. On a secured handle TcpRecv
blocks until data arrives, the peer closes, or thirty seconds pass.
"" means end of input and only that, with
TcpEof confirming it. The timeout is there so a hung server
dies with a line number instead of freezing the run.
If Ready(s) Then ' refused on a secured socket
Print(Recv(s, 4096))Print(Recv(s, 4096)) ' blocks until there is an answerScribblerOpen: no window backend — scribbler programs require `mill run`
ScribblerWait: no window backs this scribbler — the wait would never wake
ScribblerOpen: size must be at least 1x1, got {w}x{h}
Windows exist only under mill run (and the editor's
debugger). A woven program run with bare dotnet has no window
backend — no part of the mill present to create a window. So weave
console programs, and run graphical ones with mill run.
mill weave sketch.shoddy
dotnet sketch.dll ' no window backendmill run sketch.shoddyScribblerSave: cannot write '{path}'
Path, permission, or a folder that is not there.
ScribblerSave creates the file but not the directories above
it. It touches no window, so this is the ordinary file-writing failure and
nothing else. It happens the same way headless (with no screen at all),
under --no-window, and with the picture on screen.
Def Main()
Let sc = NewPlot(320, 200)
Let s = ScribblerSave(sc, "out/chart.png") ' out/ may not existDef Main()
Let sc = NewPlot(320, 200)
Let s = ScribblerSave(sc, "chart.png") ' beside the program
Print("SAVED")Sound: frequency must be positive, got {f}
Sound: duration must be >= 0 ms, got {ms}
SoundQueue: frequency must be positive (or 0 for a rest), got {f}
SoundQueue: duration must be >= 0 ms, got {ms}
SoundQueue: more than {n} minutes queued ahead on channel {ch} — feed long scores incrementally from Tick events
SoundGain: volume must be 0..1, got {v}
SoundWave: wave must be 0 (square), 1 (triangle) or 2 (sine), got {w}
NoteOn: frequency must be positive, got {f}
{who}: channel must be 1..8, got {c}
Sound argument validation. Frequencies are in hertz and must be
positive (a SoundQueue frequency of 0 is a rest). Gain is
between 0 and 1. The queue is capped so a runaway score can't buffer
unbounded audio.
Def Main()
SoundGain(1, 3) ' 0..1, not 0..10
NoteOn(1, -440)Def Main()
SoundGain(1, 0.3)
NoteOn(1, 440)internal: cannot weave node {t}
internal: cannot fold node {t}
internal: cannot render constant {what}
internal: generated C# failed to compile (source dumped to {path})
Anything beginning internal: is a toolchain bug, not a
program bug — the dump path in the last holds the evidence. Please report
any of them.
The three shapes and the golden-suite testing rule live on
the toolchain page; the linter's doctrine in
The linter. Machine-specific stops — what
makes Split or MoneyVal or
LoadMpsModel give up — are on each
machine's own page.