Envzn User Guide
Status: Extracted 2026-07-28 from ENVZN_CONSTITUTION.md, where this material stood as Article III. Its section labels — III.A, III.B, III.C — are carried over unchanged, so existing cross-references keep resolving. The Constitution cites them as ENVZN_USER_GUIDE.md III.C, to distinguish them from its own Article III, the grammar.
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.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Section III.A — 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. Concatenation with + is syntactically fine for joining two pieces. But a template keeps the literal text and the inserted values visually separate, allocates once rather than once per join, and reads in a single scan. 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 INTO String), (total INTO String))
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.
Section III.B — 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.
Section III.C — 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):
ASSERT(cond) => "msg"— the dev-only correctness tier. Active in any non-production build; stripped entirely under-prod(no condition evaluation, zero cost).ASSERT!(cond) => "msg"— the always-on production-invariant tier. Survives-prod— the!marks an assertion that fires in production (it is not "halts"; all three halt).UNREACHABLE! => "msg"— the always-on, unconditional, diverging impossible-marker (the exhaustive-MATCHdefault, the NYI stub, a dead branch). The message is optional, and the analyzer treats it as a control-flow terminator, so it satisfies definite-return / exhaustiveness in a branch that cannot produce a value.
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.
Section III.D — 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
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 6:19-24. 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.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Section III.E — Working in an editor
(New 2026-08-10, DESIGN_QUEUE #30.) The same diagnostics of III.D, 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.