Shoddy Documentation

Errors & Warnings

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 errorslayout & blocks · declarations · expressions & quotations · Select Case & patterns  |  2. Lint warnings  |  3. Runtime errorstypes & stack · arithmetic · sequences · files, network & devices

0. Setup & environment — before the mill speaks

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:

Broken
$ dotnet --list-runtimes
Microsoft.NETCore.App 8.0.11 [...]
$ mill run hello.shoddy
You must install .NET to run this application.
Fixed
' 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.

Broken
$ mill run hello.shoddy
mill: command not found
Fixed
$ ./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 hand

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

Broken
$ 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.
Fixed
' 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.

Broken
PS> .\build.ps1 build
.\build.ps1 cannot be loaded because running
scripts is disabled on this system.
Fixed
PS> powershell -ExecutionPolicy Bypass -File .\build.ps1 build

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

Broken
$ echo $SHODDYLIB
/old/checkout/machines      # moved months ago
$ mill run game.shoddy
ERROR (game.shoddy:1): cannot open 'seq.shoddy'
Fixed
$ unset SHODDYLIB           # let the mill's own
$ mill run game.shoddy      # library resolve it

1. Weave errors — the build stops

Layout and blocks

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

Broken
Def Greet()
    Print("HELLO")
      Print("WORLD")   ' deeper for no reason
Fixed
Def Greet()
    Print("HELLO")
    Print("WORLD")     ' one body, one depth

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

Broken
Def Sign(n As Number) As String
    If n < 0 Then
    "NEGATIVE"        ' body not indented past If
    Else
        "NOT NEGATIVE"
Fixed
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.

Broken
Def Fee(age As Number) As Number
    If age < 12 Then
        5
        Else          ' deeper than its If
        9
Fixed
Def Fee(age As Number) As Number
    If age < 12 Then
        5
    Else              ' same column as If
        9

IF 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).

Broken
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
        10
Fixed
Def 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
        n

DEF 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 ).

Broken
Def (n As Number) As Number   ' no name
    n * 2

Print("HI")     ' a statement at top level
Fixed
Def Twice(n As Number) As Number
    n * 2

Def Main()
    Print("HI")   ' statements live in bodies

DEF {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.

Broken
Type Point
X As Number      ' fields at the margin
Y As Number

Def Origin() As Point
Point(0, 0)      ' body at the margin
Fixed
Type 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.

Broken
Def Area(w As Number, h As Number As Number
    w * h
Fixed
Def Area(w As Number, h As Number) As Number
    w * h

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

Broken
Include "machines/sqe.shoddy"   ' typo, wrong path
Fixed
Include "seq.shoddy"   ' bare name finds the library

unterminated string literal

A string opened and the line ended before its closing quote. Strings are single-line. A quote inside a string is written \".

Broken
Print("HELLO)   ' the quote never closes
Fixed
Print("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.

Broken
Def Main()
    Print(Sum({ 1, 2, 3 })   ' one ) short
    Print("DONE")            ' swallowed as continuation
Fixed
Def 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.

Broken
Let name$ = "ADA"

Def Empty?(xs As List Of t) As Boolean
    IsEmpty(xs)
Fixed
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.

Broken
Def Next(count As Number) As Number
    If count>0 Then
        count+1
    Else
        0
Fixed
Def 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.

Broken
Def Main()
    Print("HELLO");
    Print(C:\notes.txt)
Fixed
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.

Broken
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 names
Fixed
Include "seq.shoddy"
Include "vt100.shoddy" As T
Include "util.shoddy" As A    ' one file, one name

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

Broken
Print(Camp() In N)   ' a call is not a plain name
Fixed
Print(Camp In N)     ' resolves, then calls

Declarations

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

Broken
Def Total(xs As List Of Number) As Number
    Fold(xs, 0, +)

Let total = 100    ' same word, case-folded
Fixed
Def 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.

Broken
Def Str(n As Number) As String
    "#" & "?"      ' STR is the builtin
Fixed
Def 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.

Broken
Include "net.shoddy"
Include "turtle.shoddy"   ' both export CLOSE
Fixed
Include "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:

Broken
# machines/bin holds Isam.dll but
# someone deleted Seq.dll beside it
Fixed
./build.sh machines     # Unix
./build.ps1 machines    # Windows

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

Broken
' mill run lib.shoddy
Def Capped(n As Number) As Number
    Min(n, 10)
Fixed
' either add an entry point...
Def Main()
    Print(Capped(99))

' ...or build it as what it is:
'   mill machine lib.shoddy

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

Broken
' 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 computed
Fixed
Let 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 + 1

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

Broken
Type Student
    Name As String
    Name As Number     ' duplicate field
    Score              ' As Type missing
Fixed
Type Student
    Name  As String
    Score As Number

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

Broken
Type Coin = Heads Tails |   ' missing |, then trailing
Fixed
Type Coin = Heads | Tails

Expressions, calls, and quotations

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

Broken
Def Main()
    Print(Lenght("HELLO"))   ' typo

    Print(Trim("  HI  "))    ' str never included
Fixed
Include "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.

Broken
Include "stats.shoddy"

Def Main()
    Print(Mean(ZipWith({ 1, 2 }, { 3, 4 }, +)))   ' ZipWith is seq's
Fixed
Include "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.

Broken
Include "dict.shoddy"

Def Fst(p As Pair) As Number
    Select Case p
        Case Pair(a, b)
            a
Fixed
Include "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.

Broken
Include "north.shoddy" As N
Include "south.shoddy" As S
' both declare Def Camp()

Def Main()
    Print(Camp())
Fixed
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.

Broken
Type Student
    Name  As String
    Score As Number

Def Main()
    Print(Student("ADA"))   ' Score missing
Fixed
Type 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.

Broken
Type Student
    Name  As String
    Score As Number

Def Bump(s As Student) As Student
    With(s, Points = 100)   ' no such field
Fixed
Type 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.

Broken
Def Main()
    Print(1 + )        ' operand missing
    Let x = 2 +        ' line ends mid-expression
        3
Fixed
Def Main()
    Print(1 + 2)
    Let x = (2 +
        3)             ' parentheses carry the wrap

missing ] (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.

Broken
Def Main()
    Print(Map({ 1, 2 }, [ Dup If ]))
Fixed
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.

Broken
Def Main()
    Let 2x = 10        ' names start with a letter
    Let y = 1 Let z = 2   ' one per line
Fixed
Def Main()
    Let doubled = 10
    Let y = 1
    Let z = 2

TAKE 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, …) =>.

Broken
Def Twice(n As Number) As Number
    Take m           ' Take in a call-style body
    m * 2
Fixed
Def Twice(n As Number) As Number
    n * 2

Def Twice2 ( Number -- Number )
    Take n           ' Take at home, stack-style
    n * 2

Select Case and patterns

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

Broken
Def Grade(s As Number) As String
    Select Case          ' scrutinee missing
        Case Is >= 90
            "A"
        "B"              ' body without a Case
Fixed
Def 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.

Broken
    Select Case n
        Case Else
            0
        Case 1        ' after the default
            1
Fixed
    Select Case n
        Case 1
            1
        Case Else
            0

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

Broken
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
            n
Fixed
Type Student
    Name  As String
    Score As Number

Def Show(s As Student) As String
    Select Case s
        Case Student(n, _)   ' _ ignores Score
            n

2. Lint warnings — the build continues

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

Broken
Include "seq.shoddy"
Include "money.shoddy"    ' nothing below names a money word

Def Main()
    Print(Length(Taken({ 1, 2, 3 }, 2)))
Fixed
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.

Broken
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 c
Fixed
Type Census
    Aloft  As Number
    Landed As Number

Def Report(c As Census) As Number
    Let bonus = 99
    Landed(c) + bonus

Def '{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.)

Broken
Type Crate
    Weight As Number
    Sealed As Boolean

Def Sealed(c As Crate) As String   ' the word wins
    "STAMPED"
Fixed
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.

Broken
Def Walk(rest As List Of Number) As Number
    If IsEmpty(rest) Then
        0
    Else
        1 + Walk(Rest(rest))   ' the local wins: never advances
Fixed
Def 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).

Broken
Def Inc(n As Number) As Number
    n + 1

Def Main()
    Print(Inc(1, 2))     ' one param, two arguments
Fixed
Def 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).

Broken
Def Total(a As Number, b As Number) As Number
    a
    b            ' two values left behind
Fixed
Def Total(a As Number, b As Number) As Number
    a + b        ' one value out

Def '{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.

Broken
Def Third ( Number Number -- Number )
    Drop
    Drop
    Drop        ' three drops, two values given
Fixed
Def Third ( Number Number Number -- Number )
    Drop
    Drop
    Drop        ' the signature now supplies three

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

Broken
Def Fee(vip As Boolean) As Number
    If vip Then
        0
    Else
        Print("FULL PRICE")   ' prints, yields nothing
Fixed
Def Fee(vip As Boolean) As Number
    If vip Then
        0
    Else
        9

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

Broken
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 minus
Fixed
Def IsOdd(n As Number) As Boolean
    n Mod 2 = 1          ' infix

Def Wave(z As Number) As Number
    Sin(0 - z * z)       ' subtract from zero

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

Broken
Def Double(n As Number) As Number
    n * 2

Def Main()
    Print(Map({ 1, 2 }, Duoble))   ' typo, quoted
Fixed
Def 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.

Broken
Def Ok(a As Number, b As Number) As Boolean
    a > 0
    And b > 0      ' a fresh statement, not a wrap
Fixed
Def 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.

Broken
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 70
Fixed
Type Speed = Slow | Fast

Def Limit(s As Speed) As Number
    Select Case s
        Case Slow
            30
        Case Else
            70

Def Main()
    Print(Limit(Slow()))   ' 30

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

Broken
' colors.shoddy — included As C
Let limit = 16

' sizes.shoddy — included As S
Let limit = 100
Fixed
' colors.shoddy — included As C
Def Limit() As Number
    16

' sizes.shoddy — included As S
Def Limit() As Number
    100

3. Runtime errors — the program stops

Types and the stack

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

Broken
Def Main()
    Let n = 3
    If n Then            ' a number is not a boolean
        Print("YES")
Fixed
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.)

Broken
Def Sum2 ( Number Number -- Number )
    + +          ' second + has one value, needs two
Fixed
Def 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.

Broken
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 X
Fixed
Type 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.

Broken
Def Name(k As Number) As String
    Select Case k
        Case 1
            "ONE"
        Case 2
            "TWO"
' Name(3) aborts: no matching CASE
Fixed
Def Name(k As Number) As String
    Select Case k
        Case 1
            "ONE"
        Case 2
            "TWO"
        Case Else
            "MANY"

Arithmetic and domain

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.

Broken
Def Share(total As Number, people As Number) As Number
    total / people        ' people may be 0
Fixed
Def Share(total As Number, people As Number) As Number
    If people = 0 Then
        0
    Else
        total / people

VAL: '{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.

Broken
Def Main()
    Let age = Val(Input("AGE? "))   ' "ten" aborts
Fixed
Def AskAge() As Number
    Let s = Input("AGE? ")
    If IsNumeric(s) Then
        Val(s)
    Else
        AskAge()          ' re-ask until it parses

Sequences and strings

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

Broken
Def Sum(xs As List Of Number) As Number
    First(xs) + Sum(Rest(xs))   ' no floor
Fixed
Def 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.

Broken
Def Main()
    Let xs = { "A", "B", "C" }
    Print(Nth(xs, 0))     ' there is no zeroth
Fixed
Def Main()
    Let xs = { "A", "B", "C" }
    Print(Nth(xs, 1))     ' first is 1
    Print(Nth(xs, Length(xs)))   ' last is Length

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

Broken
Def Main()
    Print(Map({ 1, 2 }, Fn(n) => Print(n)))
Fixed
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.

Broken
Def Main()
    Let xs = { 1, Print("TWO"), 3 }
Fixed
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.

Broken
Def Main()
    Print(Sort({ 3, "TWO", 1 }))
Fixed
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.

Broken
Def Main()
    Let s = "HELLO"
    Print(Asc(Mid(s, 6, 1)))      ' aborts naming ASC, not the 6
Fixed
Def 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 string

FROMCODES: 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.

Broken
Def Main()
    Print(FromCodes(ToArray({ 72, 65536 })))
    Print(InstrFrom("HELLO", "L", 0))
Fixed
Def Main()
    Print(FromCodes(ToArray({ 72, 73 })))    ' HI
    Print(InstrFrom("HELLO", "L", 1))        ' 3
    Print(InstrFrom("HELLO", "L", 9))        ' 0, not an error

Files, records, network, and devices

READFILE: 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.

Broken
Def Main()
    Print(ReadFile("scores.txt"))   ' may not exist
Fixed
Def 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.

Broken
Def Main()
    WriteFile("../../notes.txt", "hello")
    ' ERROR: WRITEFILE: '../../notes.txt' is outside the file root
Fixed
Def Main()
    WriteFile("notes.txt", "hello")           ' in the root
    WriteFile("saved/notes.txt", "hello")     ' and below it

GETNUM: 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.

Broken
Def ReadAll(f As Number)
    Print(GetNum(f))
    ReadAll(f)          ' reads past the end
Fixed
Def 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.

Broken
Def Main()
    Let f = BOpen("data.bin")
    BClose(f)
    Print(GetNum(f))     ' used after close
Fixed
Def Main()
    Let f = BOpen("data.bin")
    Print(GetNum(f))
    BClose(f)            ' close last

Input: 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.

Broken
Def Loop(sc As Scribbler) As Scribbler
    Let cmd = Input("> ")   ' window is open
Fixed
Def 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.

Broken
mill run chat.shoddy
Fixed
mill run --allow-net chat.shoddy

TCPCONNECT: '{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.

Broken
Let s = Connect("93.184.216.34", 443)
Secure(s, "93.184.216.34")   ' an IP is not a certificate name
Fixed
Let 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.

Broken
If Ready(s) Then          ' refused on a secured socket
    Print(Recv(s, 4096))
Fixed
Print(Recv(s, 4096))      ' blocks until there is an answer

ScribblerOpen: 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.

Broken
mill weave sketch.shoddy
dotnet sketch.dll        ' no window backend
Fixed
mill run sketch.shoddy

ScribblerSave: 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.

Broken
Def Main()
    Let sc = NewPlot(320, 200)
    Let s = ScribblerSave(sc, "out/chart.png")   ' out/ may not exist
Fixed
Def 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.

Broken
Def Main()
    SoundGain(1, 3)      ' 0..1, not 0..10
    NoteOn(1, -440)
Fixed
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.