Skip to content
GitHub
ReferenceStrictness

Strictness

The rules that make Calx's checker stricter than Rust's, and why.

All 2 examples tested by CIEdit this pageReport a problem

Calx’s checker is stricter than Rust’s. Silent conversions and precedence slips are where exact results quietly break, in ported code most of all, so the compiler rejects them. Every rule is an error, and no flag turns one off. There are no warnings, except for deprecated APIs once there are any.

Each rule is on the page of the feature it’s about:

Rule Page
T(x) only converts exactly. Lossy conversions are named: wrap, saturate, nearest Conversions
A conversion is exact only if it is at both of usize’s widths Conversions
Indexes are usize Types
A literal must fit its type, hex included. Unsigned types take no negative literal Integers
A constant expression that overflows is an error Integers
A constant shift count must be under the type’s width Integers
Comparing with a NaN constant is an error Floats
Unused variables, parameters, functions, structs and constants are errors, and so are a var that never changes and a mut parameter that’s never changed Variables
No shadowing Variables
Code that can never run is an error Control flow
A function returns with return, on every path Functions
A T never becomes a T? by itself: write some(x). An optional compares only with another optional Optionals
?? next to another operator needs parentheses Optionals
A match covers every value, and every arm can match Patterns
A capitalized name in a pattern is a variant or constant, never a binding. A record pattern names every field or ends in .. Patterns
let without else needs a pattern that always matches, and let ... else and if let one that might not Patterns
A value can’t be used after it moves, or after it might have moved, and a move in a loop needs a new value before the next pass Ownership
Only what a function owns can move: views, loop elements and pattern names that only look can’t. Neither can list elements or fields of views Ownership
A value can’t change or move while a pattern’s names are views into it Ownership
What a for loop goes over can’t change or move during the loop, nor can what holds it or what’s in it Control flow
In for x in mut xs, nothing reaches xs but x, to read or to change, and the loop must change x Control flow
A part of a text is a byte range that’s in order, in the text, and where runes start, checked when it runs or, for a literal, by the checker Strings
.copy() is only for values that own memory, and [x; n] copies only a Copy x Ownership
An arena list is only a variable or a view parameter, holds only Copy elements, is made only in a let of its own, and keeps its arena from changing or moving until its block ends Arenas
Generic code is checked where it’s written and uses only what its bounds give. A call whose types can’t be worked out is an error, never a guess. Unused generic parameters are errors Generics
An impl matches its interface exactly, parameter names included, and has nothing else. Unused impls are errors. A name two interfaces share needs Iface.method(x) Interfaces
A type has one function of each name: its own impl can’t reuse a name its interfaces give it Interfaces
Unused imports are errors, and an import can’t shadow a name Modules
Private fields and methods stay in their module, and a pub signature or field shows only pub types Modules
An impl goes in its type’s module or its interface’s Modules
An attribute that isn’t the compiler’s or an imported @attribute function is an error, wherever it’s written, rather than ignored Attributes
An attribute’s arguments are literals that fit its parameters’ exact types, and it goes only on the kind of declaration it reads. An attribute on a field must be one its declaration’s attribute lists Attributes
Every unsafe block does something unsafe, and none is inside another. A pointer is only a value of its own, inside unsafe Unsafe code
A C function takes and returns only C types, and has one signature wherever it’s declared C functions
A function found in a library has the type asked for, down to every struct’s fields in both builds, or isn’t found. A library is from the same build of Calx as the program that opens it Libraries

An expression on its own line must have the unit type (). A value nobody wants is discarded explicitly with _ = f(), so an ignored result is always visible.

Doesn't compile, on purpose
fn three() -> i32 {
return 3
}
fn main() {
three()
error: this i32 value is unused; discard it with _ = ...
_ = three()
}

The result of a call that can fail can’t be discarded at all, not even with _ =: its error must be passed up with try, matched, or unwrapped with ! (Errors).

These mixtures need parentheses, since their precedence surprises people:

  • and with or: a or (b and c)
  • &, | and ^ with each other: (a & b) | c
  • a shift with + or -: 1 << (n - 1), the classic slip
  • ?? with any other operator: (a ?? 0) + 1

Comparisons don’t chain: a < b < c doesn’t compile.

Doesn't compile, on purpose
fn main() {
let a = true
let n = 3
let _x = a or a and a
error: and and or together need parentheses
let _y = 1 << n - 1
error: a shift next to + or - needs parentheses
}

A literal true or false as a condition is an error, since one branch could never run, and while true is written loop.

The rest of the strictness rules apply to features that aren’t built yet, and are in the spec.