Compile time · Lesson 23.5

Compile error

Source
Stop a build with #Error, raise a warning with #Warn, and silence one lint rule with #Allow.
You'll need: When, Target, Tooling

Some mistakes are better caught by the compiler than by whoever runs the program. A program that needs 64-bit pointers should refuse to be built for anything else, with a message that says why, instead of building and then misbehaving. A function that has a better replacement should tell everyone who still calls it. These are messages from the source code to the person building it, and three directives carry them:

DirectiveEffect
#Error("…")stops the build with that message
#Warn("…")prints a warning and lets the build carry on
#Allow("rule")silences one rux lint rule for the declaration below it

Stopping the build with #Error

#Error stops the build wherever the compiler reaches it. So it is almost always written inside a when branch — the branch a build should never take:

when #target.pointerBits < 64 {
    #Error("this program needs a 64-bit target");
}

Every target Rux supports today has 64-bit pointers, so this branch is never taken, and a branch that is not taken is never looked at: the directive in it never fires. Flip the condition to >= 64 and the build stops on the spot, pointing at the directive:

Src/Main.rux:35:9: error: this program needs a 64-bit target

Used like this, #Error is a statement, so it is imported from Core like any other name: import Core::{ #Error, #target };. Its message must be a string literal written right there — the compiler prints it while compiling, long before any variable has a value.

Warning every caller with #Warn

Written above a declaration, as an attribute, a directive fires at every use of that declaration rather than where it is written. That is how an old function points its callers at the new one:

// Fires at each call below, as a warning; the program still builds and runs.
#Warn("Average rounds toward zero; call AverageRounded instead")
func Average(total: int, count: int) -> int {
    return total / count;
}

The warning at the top of the output below comes from this attribute. It points at line 38, column 41 — the call to Average in Main, not the declaration — and the program still builds and runs. #Error works as an attribute too: the build then stops at the first call, which is how a removed function can explain where to go instead of just vanishing.

As an attribute, neither directive needs an import; that is why the program imports #Error but not #Warn. Written as a statement inside a body, #Warn("…"); is imported from Core exactly like #Error.

Written asFiresImport from Core
a statement, #Error("…");where it is reached; stops the buildyes
a statement, #Warn("…");where it is reached; build carries onyes
an attribute on a declarationat every use of that declarationno

Silencing one lint rule with #Allow

rux lint checks style rules the compiler does not. One of them asks for PascalCase constant names, which would turn kilobytes (kB) into KB. The spelling is deliberate here, so the rule is switched off for this one declaration and nowhere else:

#Allow("naming.const")
const kB: int = 1000;

Without the attribute, rux lint reports warning: constant name 'kB' should be PascalCase and suggests renaming it to KB. The rules #Allow accepts are naming.type, naming.const and docs.missing.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Some mistakes are better caught by the compiler than by whoever runs the program. Three
// directives let source code speak to the build:
//
// - `#Error("...")` stops the build with that message, wherever it is reached.
// - `#Warn("...")` prints a warning and lets the build carry on.
// - `#Allow("rule")` silences one `rux lint` rule for the declaration it is attached to.
//
// `#Error` is almost always written inside a `when` branch: the branch a build should never take.
// A branch that is not taken is never looked at, so the directive in it never fires. Written as
// an attribute on a declaration instead, `#Error` or `#Warn` fires at every use of that
// declaration, which is how an old function points its callers at the new one.
import Core::{ #Error, #target };
import Io::PrintLine;

// Fires at each call below, as a warning; the program still builds and runs.
#Warn("Average rounds toward zero; call AverageRounded instead")
func Average(total: int, count: int) -> int {
    return total / count;
}

func AverageRounded(total: int, count: int) -> int {
    return (total + count / 2) / count;
}

// `rux lint` asks for PascalCase constant names, which would turn kilobytes (kB) into KB. The
// spelling is deliberate here, so the rule is switched off for this one declaration.
#Allow("naming.const")
const kB: int = 1000;

func Main() -> int {
    // Every supported target has 64-bit pointers, so this branch is never taken. Flip the
    // condition to `>= 64` and the build stops with:
    //   Src/Main.rux:35:9: error: this program needs a 64-bit target
    when #target.pointerBits < 64 {
        #Error("this program needs a 64-bit target");
    }

    PrintLine("Average of 7 and 8: {}", Average(15, 2));
    PrintLine("Rounded average: {}", AverageRounded(15, 2));
    PrintLine("A 3 kB file holds {} bytes", 3 * kB);
    return 0;
}

Besides Io, its Rux.toml lists Core under [Dependencies].

Run it

cd Examples/CompileTime/CompileError
rux run
Src\Main.rux:38:41: warning: Average rounds toward zero; call AverageRounded instead
  38 |     PrintLine("Average of 7 and 8: {}", Average(15, 2));
     |                                         ^
  note: compiler phase: Analyzing
Average of 7 and 8: 7
Rounded average: 8
A 3 kB file holds 3000 bytes

The compiler prints the file's full path where this shows Src\Main.rux.

Common mistakes

An #Error with no when around it.
A directive that is always reached always fires. Written straight into a body, #Error("…"); stops every build, on every target. Put it in the branch that should never be taken.
A message that is not a literal.
#Warn(Message); with Message a constant fails with error: '#Warn' message must be a string literal. Write the text in place.
The statement form without its import.
Inside a body, #Warn("…"); without importing it fails with error: name '#Warn' is not defined in this scope. Add it to the import Core::{ … } line.
Misspelling a lint rule.
#Allow("naming.konst") stops the build with error: unknown lint rule 'naming.konst'; valid rules are: naming.type, naming.const, docs.missing.

Try it yourself

  1. Flip the condition to when #target.pointerBits >= 64 and build. Then flip it back.
  2. Delete the #Allow line, run rux lint, and read the suggestion.
  3. Replace the #Warn on Average with #Error("Average was removed; call AverageRounded instead"). Where does the build stop? Change the call to make it build again.
  4. Add a statement #Warn("remember to update the scores table"); at the top of Main, and the import it needs.

Learn more

  • Error, Warn and Allow in the Rux Reference
  • #Error and #Warn in the Core API reference
  • rux lint — the rules #Allow can silence
  • Tooling — rux fmt, rux lint and friends
  • Extern — an #Error that explains why a build for the wrong system stops