Envzn User Guide

Status: Extracted 2026-07-28 from ENVZN_CONSTITUTION.md, where this material stood as Article III. Renumbered 2026-09-21 to a plain outline — 1, a, i — because a guide is read in order rather than cited by label, and carrying the Constitution's lettering into a document that is explicitly not normative only invited the two to be confused.

Note for anyone chasing an old reference: the Constitution still cites ENVZN_USER_GUIDE.md III.C in a few places. That was §III.C, Testing, which is now §3. The mapping is positional throughout: III.A→1, III.B→2, III.C→3, III.D→4, III.E→5.

Authority: Non-normative. Nothing here constrains a conforming implementation or adds to the language. Where this guide and Article I of the Constitution disagree, Article I wins.


This guide collects the idioms, organizational conventions, and testing practices that experienced Envzn code follows. It is advice, not rule — a program that ignores every word of it is no less correct for doing so.

1. Common Language Patterns

The idiomatic shapes for recurring tasks, each built entirely from the constructs of Article I — guidance for the reader, not new rules.

a. The patterns below are the idiomatic way to express recurring tasks in Envzn. They are not new rules; every one is built from the constructs of Article I. But they are the shapes a reader of an Envzn codebase should expect to see, and writing against them is what keeps a codebase legible to the next person.

b. For composing text, reach for the ->format() template of I.F.vi in nearly every case. There is no + on String — text has no arithmetic, so "Hello, " + name is a compile error, just as "Hello, " - name would be. Write ("Hello, $1")->format(name) even for two pieces: a template keeps the literal text and the inserted values visually separate, allocates once, and reads in a single scan. Text built up in steps goes in a DynamicString. format() converts its arguments for you — pass a number, a boolean, an enum case directly; there is no pre-conversion step, and an EMPTY handle renders as "EMPTY". The four text and binary containers of I.D.vi reach the print surface by different routes, and the routes are worth keeping straight. A String is passed directly. A DynamicString is snapshotted with toString() first. A ByteBuffer or DynamicByteBuffer is bridged through an explicit conversion: HexCodec for hexadecimal opaque-byte display, or ByteBuffer INTO String (on TextConverter) for a validated text interpretation. Binary data has no canonical text form, so the conversion should name its intent at the call site.

String msg := ("Processing item $1 of $2")->format(current, total)

DynamicString d := CREATE()
d->append("partial")
d->append(" message")
printline(d->toString())               // snapshot, then print

c. For control flow, let a conditional tree that has grown past two or three levels become a MATCH — the compiler warns at the same threshold for the same reason. To work with a value that may be EMPTY, choose by what the code needs. A full MATCH when the present and absent cases call for genuinely different logic. The ?? operator when the code wants a value and a default will do. An IS VALID guard when it wants to branch on presence without binding.

MATCH person.middleName {
    WHEN String name: { printline(name) }
    WHEN EMPTY:       { printline("no middle name") }
}

String display := middleName ?? "N/A"

d. For ownership, let the assignment operator carry the intent: = for a fresh value or a literal, := for a move or a clone, =@ for a non-owning reference. When a value enters a collection, choose the bare mutator to move it and the *Copy mutator to keep the source valid. Make that choice deliberately, rather than defaulting to one. For a recoverable failure, consume a pipe-XOR call with the IF-it-is-the-condition form of I.M, and handle a STATUS with a MATCH when more than one variant matters. Panic only for a genuine defect or a truly exceptional condition, and let the recoverable vocabulary carry the ordinary outcomes.

IF (portString INTO int32) THEN {
    connectToPort($RETURNED)
} ELSE {
    printline($!) ; useDefaultPort()
}

e. For dispatch over a closed set of related types, a GROUP matched with MATCH is the idiom. For a closed set of named constants, use an ENUM, with an interface when the cases need behaviour. For traversal, FOR IN over a collection is the default, and the explicit LOOP forms are for the cases that genuinely need index tracking or peek-ahead.

2. Reflexes that catch backsliding

The half-dozen habits a developer arriving from another language brings with them, and what Envzn does instead. Each is a compile error rather than a style point, so the compiler will tell you — but knowing them first saves the round trip.

a. . reaches into a value; -> sends a message to an instance. They are never interchangeable. A data field or a NAMESPACE member is reached with a dot — p.name, Math.PI, Math.sqrt(x) — and a method on a live instance with an arrow: p->getName(), System->exit(0). Writing p.getName(), p->name, or Math->sqrt(x) is an error in each case. The division is worth internalising because it makes the two kinds of access visible at a glance: a dot never runs code you did not write, and an arrow always might.

b. There is no null, no optional type, and no ? suffix. Absence is EMPTY, a state of a class-typed binding rather than a value it can hold. Primitives, STATUS, enums and structs are never EMPTY at all. Writing ? anywhere a type appears is a parse error, so a habit carried from Swift or Kotlin fails immediately rather than quietly.

c. == on a String is a compile error. Use ->equals() for content. On a class instance == compares references, not contents — and == on a module's own class is rejected outright, because reference identity is almost never the question being asked. == and != are value comparisons on primitives and nowhere else.

d. Methods are read-only by default; mutation is declared. This is the opposite of C++'s const: a method that writes the receiver's state must say MODIFY, and one that does not, cannot. The consequence worth planning for is that MODIFY is contagious upward — a method calling a MODIFY sibling must itself be MODIFY — so a mutation deep in a call chain surfaces in the signature of everything above it. That is the point.

e. There is no SELF. Reach a field with .field and call a method with ->method(); where another language would pass self, pass the specific field the callee needs. The pattern that replaces it is the two-layer cake: a minimal storage layer holding the data, and a richer public face composed over it — char32[] under String is the canonical example. A builder therefore accumulates through plain MODIFY methods that return nothing, and you call them on a named local, one line at a time, rather than chaining.

f. The C++ standard library is not available to you. T[] is the array, Array[T] the growable collection, String the text type, and the kernel's Dictionary family the maps. There is no std::vector, no std::string, no std::optional. This holds even in hand-written kernel C++, where the equivalent is EvArray.

g. Integer overflow panics; it does not wrap. A bare +, - or * that overflows raises a MathError rather than rolling over silently. When wrapping is what you want — a hash, a checksum, a ring buffer — say so with &+, &-, &*. Two exceptions are documented and deliberate: number promotes up its ladder rather than panicking, and decimal128 rounds.

3. Layout is part of the grammar

Envzn has no statement terminator, so the shape of the text carries meaning the compiler enforces. These are the rules that bite first, and they are compile errors rather than style advice.

a. A statement ends at a newline. There is no semicolon and no other terminator, and a stray one is rejected rather than tolerated (E1026). The end of the line is the end of the statement, which removes the entire class of missing-semicolon errors along with the character itself.

b. Two statements may not share a line. Writing int32 a = 1 int32 b = 2 is an error, not a warning. Be warned that the message is unhelpful: it surfaces as a generic E1009 parse:expression: Unexpected token in expression pointing at the second statement, rather than saying what the rule is — so if you get that error on a line that looks fine, count the statements on it.

c. A newline terminates only at bracket depth zero. Inside an unclosed ( or [ — a multi-line argument list, a wrapped compound condition, a long index or type-argument list — a newline is ordinary whitespace and the statement continues. So wrapping a long call across lines is fine and needs no continuation character.

d. Braces do not suppress termination. Inside { … } every newline ends a statement exactly as it does outside. Braces group; they do not change how statements end.

e. A one-line block is legal, with exactly one statement in it. IF (c) THEN { grade = "A" } is fine — the closing } may itself close the final statement. Two statements separated only by a space before that } fall under rule b, one-line block or not.

f. } ELSE { is an error after a multi-line block. A continuation keyword must start its own line when the block it continues spans lines (E1123):

IF (c) THEN {
    a()
} ELSE {              // ERROR — cuddled after a multi-line block
IF (c) THEN {
    a()
}
ELSE {                // correct — ELSE begins its own line

A one-line block may still cuddle, because there is no multi-line shape for the keyword to be hiding at the end of.

g. The one exception is the C-style FOR header. Its three clauses are separated by the only two semicolons the language admits outside a template qualifier, and the whole header must occupy a single line; splitting it is a parse error.

h. A lone {x} is ambiguous and rejected (E1165) — the compiler cannot tell a one-element set from a block. Write whichever you meant in a form that says so.

i. A multi-line string uses the /" delimiter (E8009), not a plain quote carried across lines.

4. Module Organization and Code Style

Conventions for laying out a module's files and writing readable Envzn, so that a module's public class surface stays visible at a glance.

a. A module is a folder, and a well-organized one keeps its public class surface visible at a glance. The grouping files — interfaces.ev, structs.ev, enums.ev — collect the type-level declarations, and every other file holds exactly one class named for the file. Related classes that might otherwise share a file each take their own, with the file's doc comment carrying any cross-class context. Subfolders by role — models, controllers, and so on — are a reasonable way to group a larger module's classes, since the module is the whole folder tree.

b. The naming conventions are conventional and worth stating only because consistency across a codebase is itself a readability feature. A type — a class, interface, struct, group, or enum — is named in PascalCase, with an acronym treated as a single word, so JsonParser rather than JSONParser. A method or a field or a local variable is named in camelCase, a method preferring an action verb and a boolean query reading as a yes-or-no question. A name is descriptive rather than abbreviated; the language already declined several abbreviations — boolean is never bool — and developer-chosen names should hold the same line.

5. Testing

The three test harnesses and how they confirm runtime behavior, given that the compiler's analyzer passes already serve as the language's lint layer.

a. Linting is built into the compiler rather than bolted on beside it: the analyzer passes carry the advisory rules, and envzn lint (I.H.viii) reports them without blocking a build. What testing adds on top is confirmation that a program does at runtime what it claims to. Three harnesses serve that, and they are the right tools in roughly increasing scope. A smoke module is a small, self-contained module whose program prints a known line; the convention is <name> ok. The smoke suite is the language-feature regression set, run after any change to the compiler. A probe module is a focused module written to verify a single claimed compiler capability, before any larger code is built on it. A probe that fails is a compiler bug, filed as such. An integration test is a multi-module arrangement that exercises dependency resolution and cross-module references together.

b. For a module's own classes, the simplest pattern is a test class whose INIT calls a series of test methods, each exercising one behavior. ASSERT is the right tool when a failed check should stop the run — it halts, so the first failure is the last thing you learn. A suite that should run to completion and report every failure wants the collect-and-report assertions described in (c) instead:

CLASS PersonTest {
    INIT() {
        testCreation()
        testEquality()
    }

    METHOD testCreation() RETURNS VOID {
        Person p := CREATE("Alice", 30)
        ASSERT(p.name->equals("Alice")) => "name should match"
        ASSERT(p.age == 30)             => "age should match"
    }
}

c. The assertion family is a distinct programmer-error failure category — not catchable by TRY/RECOVER, separate from STATUS (recoverable) and PANIC/Error (handleable). It has three statement-form constructs, all of which HALT on failure (abort without unwind — no CLEANUP runs; broken state is not run over):

The condition may reference both compile-time and runtime values, must fit on a single source line, and is separated from its message by the => token (reserved for this family). On failure the runtime writes the message and the failing site's file:line to standard error. When instrumentation is present — -debug, including the -prod -debug observable-release build — it then writes a SNAPSHOT of the in-scope variables and a STACKTRACE. Then it aborts. The non-halting, collect-and-report test-assertion role is not part of this family — it belongs to evTest's expectX and to Logger for observability.

d. Shipped V1 (2026-06-26). The three constructs above, with the THROW→PANIC / CATCH→RECOVER rename (I.M.ii) they compose with, are implemented. The FOR/IN iterator invariant (I.J.iii) moves from a defensive panic to UNREACHABLE!, resolving the "no defensive panic for can't-happen" tension. Deferred: three follow-ons. The out-of-range index and view checks (I.D.vi) migrate from the catchable IndexOutOfBoundsError to ASSERT! in V2, since that crosses the frozen _ValueArray surface. The NO_RETURN verified-divergence method modifier is deferred to V3, with the keyword reserved now. Declarative Design-by-Contract (REQUIRE/ENSURE/INVARIANT) is deferred to V2.


6. Reading a compiler diagnostic

(New 2026-08-08. What the compiler tells you when it rejects your code, and what you can do with it. The normative surface is ENVZN_IR_SPEC.md C.viii; this is the developer-facing view.)

a. The shape. A rejection points at the offending text, not merely at the line:

error [E1174] parse:numeric-literal-malformed: `0x` must be followed by hex digits
  ╭─ src/Main.ev:6:19
6 │         int32 h = 0x
  │                   ^^
  ╰─
  at src/Main.ev:6:19-21
  situation: empty_hex

  Example:    0x   ·   0b   ·   1e   ·   0b1201
  Corrected:  0x1F   ·   0b1011   ·   1.5e-3
  See: ENVZN_CONSTITUTION.md §I.C

Read it in this order: the code (E1174) is a stable identifier you can search for and quote in a bug report; the frame shows the offending text with a caret under it; Example:/Corrected: teach the shape rather than describe it; See: names the spec section that decides the rule.

b. The caret is a range, not a point. Where the compiler knows the full extent, the caret spans it and the location reads as a range — the example above is one, 6:19-21, with a caret under each column. Where it knows only where the problem starts, you get a single caret and a plain 6:19. Both are honest: a one-column caret means the compiler is not claiming an extent it has not established, not that the problem is one character wide. Coverage is still growing — many diagnostics report a point today.

c. help: is a literal edit. When a line begins help:, the text in backticks is the exact replacement — not advice, an edit:

  help: remove the semicolon

Most diagnostics have no help: line, and that is expected rather than a gap. A type error or a missing interface method is resolved by a decision only you can make; there is no substitution that is right in general. Auto-fixes are reserved for the cases where one literal edit always resolves the diagnostic.

d. No frame? Nothing is wrong. The frame is drawn only when the compiler can read the source and knows the column — a diagnostic about a manifest, a module-level rule, or a construct with no single offending token prints its location line alone. The compiler prefers a correct plain message to a confident wrong caret.

e. Machine-readable output. evc --diagnostics=json --diagnostics-out=diags.json <module> writes every diagnostic as an LSP Diagnostic — 0-based lines, UTF-16 character offsets, any fix as a TextEdit, Envzn extras namespaced under evzn. in data. The document is always written, including "diagnostics": [] for a clean build, so a tool can tell "compiled clean" from "the compiler died". This is what an editor integration consumes. Default sink is stderr; prefer --diagnostics-out — warnings render to stdout and would interleave.

f. Two flags worth knowing. --no-source-frame prints the bare form (useful when piping into a tool that does not render box glyphs). Colour is automatic on a terminal and off when piped, and honours NO_COLOR / FORCE_COLOR.

7. Working in an editor

(New 2026-08-10, DESIGN_QUEUE #30.) The same diagnostics of §4, delivered under the cursor instead of in a terminal. Everything here is a convenience — a program compiles identically with no editor support at all.

a. Install. One command, from the repo root:

./editors/install.sh              # every editor it finds
./editors/install.sh --vim        # or name them: --vim --emacs --vscode
./editors/install.sh --dry-run    # report only, write nothing

It never edits a config file you own. Where a change belongs in your init — emacs — it prints the lines and leaves the editing to you.

b. What you get.

vim emacs VS Code / Cursor CotEditor
Syntax highlighting ✓ ✓ ✓ ✓
Diagnostics (on open + save) ✓ ✓ ✓ —
Hover: the full catalog entry ✓ ✓ ✓ —
Quick fixes ✓ ✓ ✓ —
Outline, go-to-definition ✓ ✓ ✓ —
Completion <C-x><C-o> ✓ ✓ —

Highlighting is generated from the compiler's own keyword and type tables, so it cannot drift from the language. CotEditor has no LSP client, which is why its column stops at highlighting.

c. Two prerequisites, both worth checking first. The language server runs the real compiler, so it needs Python 3.10 or newer — macOS ships 3.9.6 at /usr/bin/python3, which cannot import the compiler at all — and a staged kernel (make install). The installer reports both. Get either wrong and the server does not crash; it simply declines to analyse anything, which is the confusing failure, hence the check.

d. Analysis is per MODULE, not per file. One save analyses the whole module and reports on every file in it, so an error you introduced in A.ev appears while you are looking at B.ev. Cost is roughly half a second for a small module and a few seconds for a large one; the kernel itself takes about six.

e. When it says "analysis unavailable". A buffer outside a module folder, a dependency that has not been built, or a missing staged kernel — the server names which and stops. It will not guess: a file analysed outside its module reports errors that are not real, and a confident wrong diagnostic costs more than an absent one.

f. What the editor cannot tell you. Diagnostics stop where analysis stops. Envzn's whole-program memory-safety checks (E3040 and its family) and the emit-stage diagnostics run after the point the editor's analysis reaches, so they appear only in a full build. Compile before you trust that a file is clean.

8. Gotchas worth knowing before they bite

Each of these has cost someone real time. They are not defects — every one follows from a rule elsewhere in the language — but the rule and the symptom can be far apart.

a. A T[] has no append. append and appendCopy are methods on the Array[T] class; a bare T[] is storage. You extend one by writing at the high-water mark — arr[arr.length] := value — where an index equal to the current length extends by one and an in-range index replaces. Use := for class and String elements and = for primitives. Reach for Array[T] when you actually want the richer surface.

b. Prefer T[N] whenever N is known. A fixed-size array declares N live, default-initialized slots inline in the object: no CREATE, no heap allocation, no zero-fill loop, because every numeric primitive already defaults to zero. The saving is not theoretical — one kernel scan went from 553 ms to 81 ms, a factor of six, by changing a growable int32[] field to int32[256] and deleting the loop that cleared it. Two consequences to write around: whole-array := onto a fixed array is an error, so build it in place; and T[N] and T[] are distinct types that will not assign to each other.

c. An object-element N-D array is not default-initialized. float64[m,n] arrives zeroed, but String[m,n] does not — reading a slot you never wrote is a segfault with no diagnostic. Fill it before any read.

d. A view must not outlive what it borrows. StringView and ByteBufferView borrow rather than copy, so storing one in a field or returning it as an escaping value is a compile error. A view over a parameter may be returned, because that storage belongs to the caller and outlives the call by construction.

e. IS VALID on a String or ByteBuffer is a compile error. Those types are always present — an empty one is "", not absent — so the test could never be false. Ask about content with ->isEmpty() instead. The same family of error covers REFERENCE, primitives, STATUS, enums and structs, each of which is likewise always present.

f. Dictionaries are held through the interface. Dictionary[K,V,H] is an interface with several concrete implementations behind it, chosen for the shape of your keys: a general-purpose chained hash, an ordered red-black tree, a DoS-resistant SipHash variant, and fast tiers for dense integer and byte-string keys. Hold the interface, construct the implementation.

g. One new typed variable per assignment line, and no shadowing. Declare the others first. A name already in scope may not be re-declared anywhere — not in an inner block, not in a different method of the same class where a label is involved.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━