What Makes Envzn Different

Sixteen trouble spots it closes, and eleven design choices that are rare among mainstream languages

Most of what a systems language does is settled practice, and Envzn follows it. This page covers the rest: places where a familiar language lets a defect through silently and Envzn refuses it, and places where Envzn made a choice few other languages have made. Every example below is checked by the documentation build. A block marked as failing must fail to compile with the error shown, so a claim here cannot drift from the compiler without the build noticing.

Part 1: Trouble spots other languages leave open

(a) Comparing signed with unsigned

Where it bites: C and C++. The signed side is silently converted, so -1 < 1u is false. Envzn: a mixed-sign comparison uses the mathematical values, and mixed-sign arithmetic is computed exactly, then checked against the destination's range (Constitution I.F.ii(c.v)).

int32  delta = -1
uint32 count = 1
IF delta < count THEN { printline("true in Envzn") }

int64  offset = -5
uint64 size   = 3
int64  sum    = offset + size
printline(("sum = $1")->format(sum))

(b) Integer overflow

Where it bites: undefined behaviour in C and C++; silent wraparound in Go and Java; and in Rust, checked in debug builds but wrapped in release builds, which is exactly where it ships. Envzn: overflow on any fixed-width integer, signed or unsigned, raises a catchable MathError, in production builds too. Wraparound is available, but only by asking for it with &+ &- &* (I.F.ii(a.iii), (a.iv)).

int32 big = 2147483647
int32 wrapped = big &+ 1
printline(("wrapped on purpose: $1")->format(wrapped))
TRY {
    int32 next = big + 1
    printline(("next: $1")->format(next))
}
RECOVER (MathError e) {
    printline("overflow refused")
}

(c) Shifting by the width of the type or more

Where it bites: C and C++, where the result is undefined. Envzn: a shift count must lie in [0, width), and a constant that breaks the rule is a compile error (I.F.ii(c.iv)).

int32 a = 5
int32 b = a LSHIFT 40

(d) Bitwise operators that bind looser than comparison

Where it bites: C, C++, Java and JavaScript, where x & mask == 0 means x & (mask == 0). Envzn: mixing a bitwise operator with a comparison requires parentheses.

int64 x = 6
int64 mask = 4
IF x BAND mask == 0 THEN { printline("clear") }

(e) A leading zero that silently means octal

Where it bites: C, C++ and Java, where 0755 is 493. Envzn: octal literals are refused; write decimal, 0x or 0b (I.C(e)).

int32 mode = 0755

(f) == on strings comparing references

Where it bites: Java, and C's char*, where two equal strings can compare unequal. Envzn: == on a String is a compile error; content equality is ->equals() (I.F.ii(c.i)).

String a = "hello"
String b := a->clone()
IF a == b THEN { printline("same") }

(g) Null dereference

Where it bites: C, C++, Java, Go and C#. Envzn: there is no null. A class-typed value may be EMPTY, and the compiler refuses any use it cannot prove is present; IF w IS VALID is the proof (I.D.iv).

MODULE nullExample

CLASS Widget {
    PRIVATE int32 id
    INIT(int32 i) { .id = i }
    METHOD ident() RETURNS int32 { RETURN (.id) }
}

CLASS Main IMPLEMENTS TaskStarter {
    INIT(REFERENCE String[] argv) { }

    METHOD start() RETURNS STATUS {
        Widget w = EMPTY
        printline(("id $1")->format(w->ident()))
        RETURN (SUCCESS)
    }
}

(h) Numbers used as truth values

Where it bites: C, C++, Python and JavaScript, where if (count) and if (ptr) hide a comparison. Envzn: a condition must be a boolean (I.D.i(b.iii)).

int64 count = 3
IF count THEN { printline("some") }

(i) Changing a collection while iterating over it

Where it bites: C++, where it is undefined behaviour, and Java, where it throws at run time. Envzn: a live loop or view locks its source against change, at compile time (I.D.vi(d.ii)).

int64[] items := CREATE()
items->append(1)
FOR x IN items { items->append(x) }

(j) Error results that are never checked

Where it bites: C and Go, where an error code can be stored and forgotten. Envzn: a STATUS kept in a variable must be checked before it goes out of scope. Dropping a result on purpose is written visibly, as $? := call() (I.F.iv).

MODULE statusExample

CLASS Main IMPLEMENTS TaskStarter {
    INIT(REFERENCE String[] argv) { }

    METHOD save() RETURNS STATUS {
        RETURN (SUCCESS)
    }

    METHOD start() RETURNS STATUS {
        STATUS s = save()
        RETURN (SUCCESS)
    }
}

(k) Catch-all error handlers

Where it bites: Java, C# and Python (except:), where one handler silently swallows every failure. Envzn: recovering from the base Error class is refused; name the errors you handle (I.M.ii).

TRY { printline("work") }
RECOVER (Error e) { printline("swallowed") }

(l) Silent narrowing, and casts

Where it bites: C and C++, where assigning a wide integer to a narrow one silently drops bits. Envzn: there are no casts. A lossy conversion is the explicit, fallible AS; a lossless one is INTO (I.D.viii).

int64 big = 70000
int32 small = big
int64 big = 70000
IF (big AS int32) THEN { int32 small = $RETURNED
    printline(("fits: $1")->format(small)) }
ELSE { printline($!) }

(m) Binary floating point used for money

Where it bites: almost every language, where 0.1 + 0.2 is not 0.3 and there is no built-in decimal type. Envzn: decimal128 is a built-in exact decimal type, and mixing it with a binary float is refused (I.D.i(b.iv)).

decimal128 price = 19.99
float64 tax = 0.07
decimal128 total = price + tax

(n) Bytes confused with characters

Where it bites: C and C++, where char is a byte, so text and binary data share a type. Envzn: a character (char, a code point), its UTF code units (char8, char16, char32) and a raw octet (binary) are separate types. An octet is a bit pattern, not a number: it is written in hex or binary and cannot be multiplied (I.D.i(b.i)).

binary b = 195
binary b = 0xC3
binary c = b * b

(o) Switch fall-through

Where it bites: C, C++ and Java, where a forgotten break runs the next case. Envzn: MATCH never falls through, and it dispatches only over closed sets (an enum, a GROUP of types, or a STATUS). Matching on arbitrary values is an IF chain (I.H.v).

int32 n = 5
MATCH n {
    WHEN 5: { printline("five") }
}

(p) Build steps that run arbitrary code

Where it bites: npm install scripts, Cargo's build.rs, CMake: a dependency can run code on your machine at build time. Envzn: manifest build flags that could run code are refused, inline C blocks are not part of the language, and a module may call C only after opting in with "allows_foreign": true (I.N).

{ "name": "netio", "version": "1.0.0", "target": "library", "allows_foreign": true, "sources": ["Socket.ev"] }

Part 2: Design choices that are rare

  1. Memory safety without lifetime annotations. Instead of trusting function signatures, the compiler follows each reference through the code itself, including the code of the libraries it depends on, read from their .evir files. What it cannot prove safe, it refuses (I.A.ii, I.I.v). Rust needs lifetime annotations for the same guarantee; C++ has no equivalent.
  2. Leak-freedom proven at compile time. Every object is owned, and the graph of owning and counted references must contain no cycle, so a leak is a compile error (E3033). Swift's ARC and Rust's Rc leak a cycle silently (I.I.i(b.i)).
  3. Overflow checks that stay on in production. See (b): the check is never stripped from a release build, which makes Envzn stricter than Rust on this point.
  4. Parameter modes spelled as words. A plain parameter owns its argument, REFERENCE reads it, and MUTABLE REFERENCE writes it, exclusively (I.I.vi). The same three meanings are sigils in Rust and drift between &, const& and && in C++.
  5. No casts at all. Every conversion is one of two operators: INTO, which is lossless and always succeeds, and AS, which may lose information and so may fail. Each conversion pair may be defined only once across a program (I.D.viii).
  6. Every diagnostic cites the law. All 639 compiler diagnostics carry a reference to the section of the Envzn Constitution they enforce, and many include the corrected code (I.A).
  7. Bounds-check removal you ask for, and that is proven. HOTLOOP asks the compiler to replace a loop's per-element bounds checks with one check up front. If it cannot prove that one check covers every access, the build fails (E5027) rather than run unchecked (I.H.iii(f)).
  8. Three assignment operators, one per meaning. = copies a value, := takes ownership (constructing, cloning or moving), and =@ binds a reference. A move is visible at the point it happens (I.F).
  9. Numbers built for real work. decimal128 for exact money arithmetic; number, which holds an integer or a float and promotes rather than overflows; and complex with an im literal, as in 3 + 4im (I.D.i).
  10. Column-major N-D arrays in the language. T[R, C] is laid out the way BLAS and LAPACK expect, as in Fortran, Julia and MATLAB. No C-family language does this (I.J.i).
  11. Calling C without hand-written wrappers. FOREIGN BIND declares a C function in Envzn types, and the compiler produces the call. Raw pointers stay inside UNSAFE blocks, and a module must opt in before it can call C at all (I.N, I.I.viii).