Version: 3.0.0 Date: April 2, 2026 Status: Release EZ Version: 3.0.0
Notice
This specification was generated by Claude through systematic review of the EZ language source code and passing integration tests. It represents a best-effort documentation of the language's syntax, semantics, and standard library as implemented.
This document is subject to change as the EZ language evolves. It is not guaranteed to be complete or fully accurate in all edge cases.
Primary Purpose: This specification is intended primarily to provide context for AI agents and language models to learn and understand the EZ programming language. It serves as a reference for AI-assisted development, code generation, and tooling support. While human developers may find it useful, the official EZ documentation at https://schoolyb.github.io/EZ-Language-Webapp/docs should be consulted for tutorials and usage guidance.
- Introduction
- Lexical Structure
- Types
- Variables and Constants
- Expressions
- Statements
- Functions
- Modules
- Standard Library
- Error Handling
- Memory Model
- Program Execution
- CLI Commands
This document defines the EZ programming language. It serves as the authoritative reference for the language's syntax, semantics, and behavior. Implementations of EZ must conform to this specification.
EZ is a statically-typed programming language that compiles to native binaries via the EZ compiler. The language emphasizes:
- Simplicity: A minimal set of orthogonal features
- Clarity: Explicit syntax that reads naturally
- Safety: Static type checking and runtime bounds checking
EZ source files must be encoded in UTF-8. However, identifiers are restricted to ASCII characters.
Line terminators are the ASCII line feed character (LF, U+000A) or the ASCII carriage return character (CR, U+000D) optionally followed by a line feed.
EZ supports two forms of comments:
Single-line comments begin with // and extend to the end of the line:
// This is a single-line comment
mut x int = 42 // inline comment
Multi-line comments begin with /* and end with */:
/* This is a
multi-line comment */
Multi-line comments can also be used inline within a statement:
mut x int = /* default value */ 10
Multi-line comments do not nest. A /* inside a multi-line comment has no special meaning.
Identifiers name program entities such as variables, functions, types, and modules.
identifier = letter { letter | digit | "_" } .
letter = "A" ... "Z" | "a" ... "z" .
digit = "0" ... "9" .
Identifiers must:
- Begin with an ASCII letter
- Contain only ASCII letters, digits, and underscores
- Not be a reserved keyword
Valid identifiers: x, count, myVariable, point_2d, MAX_SIZE
Invalid identifiers: 2fast, my-var, café, _private
The following words are reserved and may not be used as identifiers:
Control flow:
as_long_as break continue default
ensure for for_each if is
loop or or_return otherwise return
when while
Declarations:
const do enum import mut
new private struct use* using
*useis reserved exclusively for theimport and usestatement. It has no other syntactic role.
Types (reserved names):
bool byte char Error float
func int map nil string
uint
Sized types (reserved names):
i8 i16 i32 i64 i128 i256
u8 u16 u32 u64 u128 u256
f32 f64
Operators and values:
cast false in not_in range
true
+ - * / %
== != < > <= >=
&& || !
= += -= *= /= %=
++ --
^ & @ #
( ) { } [ ]
, : . ->
^— pointer type prefix and dereference postfix (^Type,ptr^)&— address-of (used internally)@— module prefix in imports (import @math)#— attribute prefix (#doc,#flags,#strict)
Integer literals represent integer values.
int_literal = decimal_lit | hex_lit | octal_lit | binary_lit .
decimal_lit = digit { [ "_" ] digit } .
hex_lit = "0" ( "x" | "X" ) hex_digit { [ "_" ] hex_digit } .
hex_digit = digit | "A" ... "F" | "a" ... "f" .
octal_lit = "0" ( "o" | "O" ) octal_digit { [ "_" ] octal_digit } .
octal_digit = "0" ... "7" .
binary_lit = "0" ( "b" | "B" ) bin_digit { [ "_" ] bin_digit } .
bin_digit = "0" | "1" .
Underscores may be used for readability but:
- Must not appear at the beginning or end
- Must not appear consecutively
- Must not appear adjacent to the decimal point
Examples: 42, 1_000_000, 0xFF, 0xDEAD_BEEF, 0o777, 0o1_2_3, 0b1010, 0b1111_0000
Floating-point literals represent floating-point values.
float_literal = digit { digit } "." digit { digit } .
Examples: 3.14159, 0.5, 100.0
String literals represent string values.
string_literal = '"' { string_char | escape_seq | interpolation } '"' .
string_char = /* any UTF-8 character except '"', '\', or newline */ .
escape_seq = "\" ( "n" | "r" | "t" | "\" | '"' ) .
interpolation = "${" expression "}" .
Escape sequences:
\n- line feed (U+000A)\r- carriage return (U+000D)\t- horizontal tab (U+0009)\\- backslash\"- double quote\xNN- hex byte value (e.g.,\x48= 'H',\x0a= newline)
String interpolation allows embedding expressions within strings:
mut name string = "World"
mut greeting string = "Hello, ${name}!" // "Hello, World!"
Raw string literals are enclosed in backticks and do not process escape sequences or string interpolation:
raw_string_literal = "`" { raw_string_char } "`" .
raw_string_char = /* any UTF-8 character except "`" */ .
Raw strings:
- Do not process escape sequences (
\nis a literal backslash followed byn) - Do not process string interpolation (
${x}is literal text) - May span multiple lines
- Cannot contain backticks (no escape mechanism)
mut path string = `C:\Users\test\file.txt`
mut pattern string = `\d+\.\d+`
mut multi string = `line1
line2
line3`
Character literals represent single character values.
char_literal = "'" ( char_char | escape_seq ) "'" .
char_char = /* any UTF-8 character except "'", '\', or newline */ .
Examples: 'A', '\n', '\t'
Character literals must contain exactly one character (or escape sequence).
bool_literal = "true" | "false" .
nil_literal = "nil" .
The literal nil represents the absence of a value.
EZ is statically typed. Every variable and expression has a type known at check time.
The int type represents a 64-bit signed integer (int64_t in C). Arithmetic operations use checked arithmetic — overflow or underflow produces a runtime panic rather than silent wrapping.
mut small int = 42
mut large int = 9223372036854775807 // Max 64-bit signed value
The uint type represents a 64-bit unsigned integer (uint64_t in C). Like int, arithmetic is overflow-checked with a runtime panic on overflow.
mut count uint = 100
mut big uint = 18446744073709551615 // Max 64-bit unsigned value
Assigning a negative value to a uint produces a check-time error.
The float type represents 64-bit IEEE 754 double-precision floating-point numbers.
Division by zero with floating-point operands produces a runtime panic. However, special IEEE 754 values (NaN, Infinity, -Infinity) can appear through C interop or stdlib math functions (e.g., edge cases in trigonometric or logarithmic functions). Use math.is_nan(), math.is_infinite(), and math.is_finite() to check for these values.
mut pi float = 3.14159
mut negative float = -2.5
Integer values are implicitly promoted to float when the target type is float, f32, or f64. This applies to variable declarations, assignments, function arguments, map literal values, and return statements:
mut x float = 5 // 5.0
mut y f64 = 1 // 1.0
x = 42 // 42.0
No explicit float() cast is needed. The promotion is lossless for values within the floating-point range.
The string type represents a UTF-8 encoded byte sequence. String indexing (str[i]) returns the byte at byte position i, not a Unicode codepoint. len() returns the byte length, not the character count.
For ASCII strings, one byte equals one character, so indexing works as expected:
mut greeting string = "Hello, World!"
mut first_char char = greeting[0] // 'H'
For multi-byte UTF-8 strings, individual bytes may not form complete characters:
mut s string = "日本語"
println(len(s)) // 9 (byte length, not 3 characters)
println(char_count(s)) // 3 (Unicode character count)
println(to_char(s, 0)) // 26085 (codepoint for '日')
Use to_char() to access characters by codepoint index and char_count() to get the true character count.
The bool type has exactly two values: true and false.
mut flag bool = true
mut result bool = 10 > 5 // true
The char type represents a single character.
mut letter char = 'A'
mut newline char = '\n'
Characters can be converted to their integer code point using int().
The byte type represents an 8-bit unsigned integer with values from 0 to 255.
mut b byte = 0xFF // 255
mut c byte = 128 // decimal also works
Assigning a value outside the range 0-255 to a byte is a check-time error.
EZ provides fixed-width integer types for precise control over integer size:
| Type | Width | Signed | Range |
|---|---|---|---|
i8 |
8-bit | yes | -128 to 127 |
i16 |
16-bit | yes | -32,768 to 32,767 |
i32 |
32-bit | yes | -2^31 to 2^31-1 |
i64 |
64-bit | yes | -2^63 to 2^63-1 |
u8 |
8-bit | no | 0 to 255 |
u16 |
16-bit | no | 0 to 65,535 |
u32 |
32-bit | no | 0 to 2^32-1 |
u64 |
64-bit | no | 0 to 2^64-1 |
Use cast for sized type conversions: cast(value, i32), cast(value, u16).
EZ provides portable wide integer types backed by struct-based arithmetic (no compiler extensions required):
| Type | Width | Signed | size_of |
|---|---|---|---|
i128 |
128-bit | yes | 16 |
u128 |
128-bit | no | 16 |
i256 |
256-bit | yes | 32 |
u256 |
256-bit | no | 32 |
Wide integers support all standard arithmetic (+, -, *, /, %) and comparison operators (==, !=, <, >, <=, >=). Values are constructed using the type name as a function:
mut a i128 = i128(42)
mut b i128 = i128(100)
mut c i128 = a + b // i128 addition
println(c) // prints "142"
println(type_of(c)) // "i128"
println(size_of(i128)) // 16
mut x int = int(c) // cast back to int
mut s string = string(c) // convert to string
Wide integers use the same overflow-checked arithmetic as int and uint — overflow produces a runtime panic.
The pointer type ^Type represents a memory address pointing to a value of Type.
| Syntax | Meaning |
|---|---|
^int |
Pointer to an int |
^MyStruct |
Pointer to a MyStruct |
addr(x) |
Get the address of x |
p^ |
Dereference pointer p |
mut x int = 42
mut p ^int = addr(x)
println(p) // 0x16d1ab9f8 — prints the address as hex
println(p^) // 42 — explicit dereference reads the pointee
p^ = 100
println(x) // 100
Printing a pointer value (println(p), print(p), etc.) outputs the address in hex (0x...) when non-nil and nil when null. Use p^ to access the pointee.
Dereferencing a nil pointer causes a runtime panic.
Note: The dot operator (
.) automatically dereferences pointers to structs. Ifpis a^MyStruct, writingp.fieldis equivalent top^.field. This auto-dereference applies to field access and struct function calls but does not apply in other contexts — for example,println(p)prints the address, andreturn preturns the pointer itself. Use explicitp^when you need the pointee value rather than field access.
Arrays are ordered collections of elements of the same type.
Dynamic arrays have variable length:
array_type = "[" type "]" .
mut numbers [int] = {1, 2, 3, 4, 5}
mut empty [string] = {}
Fixed-size arrays have a length specified at declaration:
fixed_array_type = "[" type "," size "]" .
const fixed [int, 3] = {10, 20, 30}
Fixed-size arrays must be declared with const. Providing fewer values than the declared size is permitted (a W3003 warning is issued); providing more values than the declared size is an error.
const a [int, 5] = {1, 2, 3} // OK — warning W3003 (3 of 5 slots used)
const b [int, 5] = {1, 2, 3, 4, 5, 6} // Error — 6 values exceeds size of 5
Multi-dimensional arrays:
mut matrix [[int]] = {{1, 2}, {3, 4}}
mut cube [[[int]]] = {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}}
Array indexing is zero-based. Accessing an index outside the valid range produces a runtime error.
Maps are unordered collections of key-value pairs.
map_type = "map" "[" key_type ":" value_type "]" .
mut ages map[string:int] = {
"alice": 30,
"bob": 25
}
mut empty map[string:int] = {:} // Empty map
Note: Empty maps use {:}, not {}. The {} literal is an empty array. The colon distinguishes the two:
mut arr [int] = {} // Empty array
mut m map[string:int] = {:} // Empty map
Maps must be declared with mut. Declaring a map with const is a compile-time error — if the keys are known at compile time, use a struct instead.
Keys must be of a hashable type: int, uint, float, string, bool, char, or byte.
Accessing a key that does not exist produces a runtime error.
Structs are user-defined composite types with named fields.
struct_decl = "const" identifier "struct" "{" { field_decl } "}" .
field_decl = identifier type .
const Point struct {
x int
y int
}
const Person struct {
name string
age int
active bool
}
Note: Struct fields must be on separate lines. Inline declarations like const Point struct { x int; y int } are not allowed. Semicolons are never used in struct or enum declarations.
Struct instantiation uses named field syntax:
mut origin Point = Point{x: 0, y: 0}
mut p Point = Point{} // Zero-initialized: x=0, y=0
Fields are accessed using dot notation:
mut x_value int = origin.x
origin.x = 10 // Modification (if variable is mut)
Enums define a type with a fixed set of named values.
Integer enums (default, auto-incrementing from 0):
const Direction enum {
NORTH // 0
EAST // 1
SOUTH // 2
WEST // 3
}
Note: Enum variants must be on separate lines. Inline declarations like const Color enum { RED; GREEN; BLUE } are not allowed. Semicolons are never used in enum declarations.
Flags enums (powers of 2, annotated with #flags):
#flags
const Permissions enum {
READ // 1
WRITE // 2
EXECUTE // 4
DELETE // 8
}
String enums (explicit string values):
const Status enum {
TODO = "todo"
IN_PROGRESS = "in_progress"
DONE = "done"
}
Enum values are accessed using dot notation:
mut dir = Direction.NORTH
mut status = Status.TODO
EZ is a statically-typed language with full type inference. The type of every variable is known at compile time, but explicit type annotations are optional when the compiler can determine the type from the initializer.
Type inference works with:
- Standalone literals - The type is inferred from the literal value
- Function return values - The variable's type is inferred from the function's return type
- Struct literals - The type is known from the struct name
- Built-in constructors -
new(Type)(returns^Type) andcopy(value) - Array literals - When element types are consistent
- Multiple return values - Each variable's type is inferred from the corresponding return type
// Inferred from literals
mut x = 42 // Inferred: int
mut name = "Alice" // Inferred: string
mut pi = 3.14 // Inferred: float
mut flag = true // Inferred: bool
// Explicit annotations are always accepted
mut y int = 42 // Explicit: int
// Inferred from function return type
do sum(a int, b int) -> int {
return a + b
}
mut result = sum(1, 2) // Inferred: int
println(type_of(result)) // Output: int
// Inferred from struct literal
const p = Point{x: 1, y: 2} // Inferred: Point
// Inferred from built-in constructors
mut val = new(Person) // Inferred: ^Person (pointer)
mut dup = copy(val) // Inferred: Person
// Inferred from array literal
mut arr = {1, 2, 3} // Inferred: [int]
// Multiple return values
do divide(a, b int) -> (int, int) {
return a / b, a % b
}
mut quotient, remainder = divide(10, 3) // Both inferred: int
Explicit type annotations are never required but can be used for clarity or documentation.
Explicit type conversions are performed using type constructors:
mut i int = int('A') // 65 - char to int (code point)
mut f float = float(42) // 42.0 - int to float
mut s string = string(123) // "123" - int to string
mut c char = char(65) // 'A' - int to char
Conversions that would lose information or are invalid produce check-time or runtime errors.
The cast keyword provides explicit type conversion for values and arrays:
cast_expr = "cast" "(" expression "," type ")" .
mut small u8 = cast(42, u8)
mut truncated int = cast(3.7, int) // 3
mut text string = cast(123, string) // "123"
For array conversions, cast converts each element to the target element type:
mut ints [int] = {1, 2, 3}
mut bytes [u8] = cast(ints, [u8])
Range constraints are enforced at runtime (e.g., u8 values must be 0-255).
Variables are declared using the mut keyword:
var_decl = "mut" identifier type "=" expression .
mut count int = 0
mut name string = "Alice"
mut items [int] = {1, 2, 3}
Variables declared with mut:
- Must be initialized at declaration
- Can be reassigned after declaration
- Are scoped to their containing block
Constants are declared using the const keyword:
const_decl = "const" identifier [ type ] "=" expression .
const PI float = 3.14159
const MAX_SIZE int = 100
const origin = Point{x: 0, y: 0}
Constants declared with const:
- Must be initialized at declaration
- Cannot be reassigned
- Cannot have their contents modified (for composite types)
- Are scoped to their containing block (or module, if at top level)
The const keyword indicates complete immutability:
const arr [int, 3] = {1, 2, 3}
arr[0] = 10 // ERROR: Cannot modify const
arr = {4, 5, 6} // ERROR: Cannot reassign const
The mut keyword allows modification and ensures the value itself is mutable:
mut arr [int] = {1, 2, 3}
arr[0] = 10 // OK
arr = {4, 5, 6} // OK
When assigning from a function return or other source, mut automatically makes the value mutable. There is no need to use copy() to obtain a mutable version:
do get_data() -> [int] {
return {1, 2, 3}
}
mut arr = get_data() // arr is mutable
arrays.append(arr, 4) // OK - can modify directly
const fixed = get_data() // fixed is immutable
arrays.append(fixed, 4) // ERROR - cannot modify const
Variables and constants are block-scoped. A block is delimited by braces {}.
Inner scopes may declare variables that shadow outer scope variables:
mut x int = 10
if true {
mut x int = 20 // Shadows outer x
// x is 20 here
}
// x is 10 here
The blank identifier _ can be used to discard values that are not needed. This is particularly useful with multiple return values:
// Discard the second return value
mut value, _ = get_pair()
// Discard multiple values
const _, middle, _ = get_triple()
// Common pattern: ignore error when you know it won't fail
mut data, _ = json.encode(simple_value)
The blank identifier:
- Cannot be read from (it discards the value)
- Can be used multiple times in the same assignment
- Works with both
mutandconstdeclarations
primary_expr = identifier
| literal
| "(" expression ")"
| array_literal
| map_literal
| struct_literal .
{1, 2, 3}
{"a", "b", "c"}
{} // Empty array (not to be confused with {:} which is an empty map)
All elements in an array literal must be the same type. Mixed-type literals are rejected at compile time (E3001):
{1, "two", 3} // error[E3001]: mixed types in array literal
See Section 3.2.2 for map literal syntax, including the {:} empty map literal.
Point{x: 10, y: 20}
Person{name: "Alice", age: 30, active: true}
Point{} // Zero-initialized
| Operator | Description | Operand Types | Result Type |
|---|---|---|---|
+ |
Addition | int, int |
int |
+ |
Addition | float, float |
float |
- |
Subtraction | int, int |
int |
- |
Subtraction | float, float |
float |
* |
Multiplication | int, int |
int |
* |
Multiplication | float, float |
float |
/ |
Division | int, int |
int (truncated) |
/ |
Division | float, float |
float |
% |
Modulo | int, int |
int |
Division by zero produces a runtime error.
| Operator | Description |
|---|---|
== |
Equal |
!= |
Not equal |
< |
Less than |
> |
Greater than |
<= |
Less than or equal |
>= |
Greater than or equal |
Comparison operators return bool.
Comparison operators only work on primitive types (numeric kinds, bool, char, byte, string for equality, enums) and pointer equality (== / != against nil or another pointer of the same pointee type). They are not defined on aggregate types:
- Arrays — use
arrays.is_equal(a, b)for equality (E3074). - Maps — use
maps.is_equal(a, b)for equality; ordering is not defined on maps (E3076). - Structs — compare individual fields (
a.x == b.x); E3077. - Pointer arithmetic and ordering are not supported (E3078); equality (
==/!=) on pointers is allowed.
| Operator | Description |
|---|---|
&& |
Logical AND (short-circuit) |
|| |
Logical OR (short-circuit) |
! |
Logical NOT |
Logical AND and OR use short-circuit evaluation: the right operand is not evaluated if the result can be determined from the left operand alone.
| Operator | Description |
|---|---|
in |
Membership test |
not_in |
Non-membership test |
!in |
Non-membership test (shorthand for not_in) |
if 3 in numbers { ... }
if "key" not_in map { ... }
if x in range(0, 10) { ... }
if 10 !in range(0, 10) { ... } // Shorthand for not_in
| Operator | Description |
|---|---|
= |
Assignment |
+= |
Addition assignment |
-= |
Subtraction assignment |
*= |
Multiplication assignment |
/= |
Division assignment |
%= |
Modulo assignment |
| Operator | Description |
|---|---|
++ |
Post-increment |
-- |
Post-decrement |
mut x int = 5
x++ // x is now 6
x-- // x is now 5
From highest to lowest precedence:
- Parentheses:
() - Unary:
!,-(negation) - Multiplicative:
*,/,% - Additive:
+,- - Comparison:
<,>,<=,>= - Equality:
==,!= - Logical AND:
&& - Logical OR:
|| - Membership:
in,not_in
mut arr [int] = {10, 20, 30}
mut val int = arr[1] // 20
arr[0] = 100 // Modification
mut str string = "hello"
mut c char = str[0] // 'h'
mut m map[string:int] = {"a": 1}
mut v int = m["a"] // 1
mut p Point = Point{x: 10, y: 20}
mut x int = p.x // 10
p.y = 30 // Modification
mut status int = Direction.NORTH // Enum access
mut sum int = add(1, 2)
mut greeting string = greet("World")
println("Hello!")
range_expr = "range" "(" start "," end [ "," step ] ")" .
range(0, 10) // 0, 1, 2, ..., 9 (increment)
range(0, 10, 2) // 0, 2, 4, 6, 8 (increment)
range(10, 0, -1) // 10, 9, 8, ..., 1 (decrement)
range(10, 0, -2) // 10, 8, 6, 4, 2 (decrement)
Ranges are inclusive of the start value and exclusive of the end value.
Step validation rules:
- Positive step (or omitted): start must be ≤ end
- Negative step: start must be ≥ end (for backwards iteration)
- Zero step: always produces a runtime panic
- Mismatched direction (e.g.,
range(0, 10, -1)) produces an error (E9005)
Any expression can be used as a statement:
println("Hello")
counter++
do_something()
if_stmt = "if" expression block { "or" expression block } [ "otherwise" block ] .
if x < 0 {
println("negative")
} or x == 0 {
println("zero")
} otherwise {
println("positive")
}
The or keyword introduces additional conditions (similar to else if in other languages).
The otherwise keyword introduces the default case (similar to else).
for_stmt = "for" [ "(" ] identifier [ type ] "in" range_expr [ ")" ] block .
for i in range(0, 10) {
println("${i}")
}
for i int in range(0, 5) {
// With explicit type
}
for (i in range(0, 10)) {
// Parentheses optional
}
for_each_stmt = "for_each" [ "(" ] [ identifier "," ] identifier "in" expression [ ")" ] block .
mut items [string] = {"a", "b", "c"}
for_each item in items {
println(item)
}
An optional index variable can precede the value variable, separated by a comma:
for_each i, item in items {
println("${i}: ${item}")
}
// Output: 0: a, 1: b, 2: c
The index variable is always of type int and is zero-based. It works with both arrays and strings:
for_each i, ch in "hello" {
println("${i}: ${ch}")
}
The blank identifier _ can be used in either position:
for_each _, item in items { ... } // discard index (same as no index)
for_each i, _ in items { ... } // index only, discard value
Map iteration is also supported. With two variables, the first is the key and the second is the value:
mut ages map[string:int] = {"alice": 30, "bob": 25}
for_each k, v in ages {
println("${k}: ${v}")
}
// Single variable iterates keys only
for_each key in ages {
println(key)
}
Map iteration order is undefined (maps are unordered).
Mutation during iteration:
- Arrays: The loop length is captured when
for_eachbegins. Appending to the array during iteration is safe — new elements are added to the array but are not visited by the current loop. The full array (including appended elements) is available after the loop ends. - Maps: Modifying a map during
for_each(inserting or deleting keys) is not allowed and will panic at runtime. Read the map freely, but do not mutate it until the loop completes.
while_stmt = ( "as_long_as" | "while" ) expression block .
while is an alias for as_long_as. Both are valid — user's choice.
mut count int = 0
as_long_as count < 10 {
count++
}
// Equivalent:
while count < 10 {
count++
}
loop_stmt = "loop" block .
The loop statement creates an infinite loop that runs until explicitly terminated with break or return:
loop {
mut input string = input()
if input == "quit" {
break
}
println("You said: ${input}")
}
The break statement terminates the innermost enclosing loop:
for i in range(0, 100) {
if i == 5 {
break
}
}
The continue statement skips to the next iteration of the innermost enclosing loop:
for i in range(0, 10) {
if i % 2 == 0 {
continue
}
println("${i}") // Prints odd numbers only
}
The return statement exits the current function, optionally returning a value:
do add(a int, b int) -> int {
return a + b
}
do greet() {
println("Hello")
return // Void return
}
when_stmt = [ "#strict" ] "when" expression "{" { when_case } [ default_case ] "}" .
when_case = "is" pattern { "," pattern } block .
default_case = "default" block .
pattern = expression | range_expr .
when x {
is 1 { println("one") }
is 2, 3 { println("two or three") }
is range(4, 10) { println("four to nine") }
default { println("other") }
}
Allowed condition types: int, uint, string, char, byte, bool, float, and enum types. Float conditions emit a W2012 warning about imprecision. Collection types (arrays, maps) are not allowed.
Strict mode requires all possible values to be handled:
#strict
when direction {
is Direction.NORTH { ... }
is Direction.EAST { ... }
is Direction.SOUTH { ... }
is Direction.WEST { ... }
}
The ensure statement specifies a function to call when the enclosing function exits (whether normally or via early return):
do process_file() {
ensure cleanup()
// ... do work ...
if error_condition {
return // cleanup() will be called
}
// cleanup() will be called when function ends
}
The or_return keyword provides error propagation shorthand for functions that return (T, Error) tuples:
or_return_stmt = var_decl "or_return" [ expression { "," expression } ] .
do load() -> (string, Error) {
// Bare or_return: propagates the error with zero values
mut content = read_file("data.txt") or_return
mut parsed = json.decode(content) or_return
return parsed, nil
}
// With custom fallback values:
mut content = read_file("data.txt") or_return "", error("failed to load")
When the call returns a non-nil error, or_return immediately returns from the enclosing function. The bare form returns zero values for non-error slots plus the original error. The enclosing function must have Error as its last return type.
func_decl = "do" identifier "(" [ param_list ] ")" [ "->" return_type ] block .
param_list = param { "," param } .
param = [ "&" ] identifier [ "," identifier ]... type [ "=" default_value ] .
return_type = type | "(" type { "," type } ")"
| "(" named_return { "," named_return } ")" .
named_return = identifier [ "," identifier ]... type .
do add(a int, b int) -> int {
return a + b
}
do greet(name string = "World") -> string {
return "Hello, ${name}!"
}
do process() {
// Void function (no return type)
}
By default, parameters are passed by value and cannot modify the caller's variables:
do double(x int) -> int {
return x * 2
}
Parameters prefixed with & can modify the caller's variables:
do increment(&n int) {
n = n + 1
}
mut count int = 0
increment(count) // count is now 1
Mutable parameters work with:
- Primitive types
- Struct fields:
increment(point.x) - Array elements:
increment(arr[0]) - Map values:
increment(map["key"])
Multiple parameters of the same type can be grouped:
do add(a, b int) -> int {
return a + b
}
do swap(&a, &b int) {
mut t int = a
a = b
b = t
}
Parameters can have default values. Default values are supported for all primitive types (int, uint, float, string, bool, char):
do greet(name string = "World") -> string {
return "Hello, ${name}!"
}
greet() // "Hello, World!"
greet("Alice") // "Hello, Alice!"
Multiple parameters may have defaults. When calling, arguments fill left-to-right and any remaining parameters use their defaults:
do connect(host string, port int = 8080, verbose bool = false) {
if verbose {
println("Connecting to ${host}:${port}")
}
}
connect("localhost") // port=8080, verbose=false
connect("localhost", 3000) // port=3000, verbose=false
connect("localhost", 3000, true) // port=3000, verbose=true
Default parameters must appear after non-default parameters. A required parameter cannot follow a parameter with a default value:
do foo(a int = 10, b int) {} // Error E2039: required parameter after default
do bar(a int, b int = 10) {} // OK: required first, then default
do square(x int) -> int {
return x * x
}
do divide(a, b int) -> (int, int) {
return a / b, a % b
}
mut quotient, remainder = divide(17, 5)
do parse(s string) -> (int, Error) {
if s == "" {
return 0, error("empty string")
}
return 42, nil
}
mut value, err = parse("test")
if err != nil {
// Handle error
}
Return values can be given names to document what each position in the return tuple represents. Named return values are labels only — they do not implicitly declare variables in the function body. The programmer must explicitly declare any variables they use:
do divide(a, b int) -> (quotient int, remainder int) {
mut quotient int = a / b
mut remainder int = a % b
return quotient, remainder
}
mut q, r = divide(17, 5) // q=3, r=2
Named returns support grouped types (multiple names sharing one type):
do get_info() -> (name, city string, age int) {
mut name string = "Alice"
mut city string = "NYC"
mut age int = 30
return name, city, age
}
Named return values must be enclosed in parentheses. The names serve as documentation for callers and tooling (e.g., ez doc) but have no effect on the function's scope or variable declarations.
Restriction: Wildcard types (?) cannot be used in named return positions (E3082). Since ? resolves to a different concrete type at each call site, the name adds no useful documentation. Use an unnamed return instead:
// Error: wildcard type '?' cannot be named
do first(arr [?]) -> (result ?) { ... }
// OK: unnamed wildcard return
do first(arr [?]) -> ? { ... }
Functions without a return type return no value:
do print_greeting() {
println("Hello!")
}
By default, all functions and constants are public. The private keyword restricts access to the declaring module:
// mathlib/mathlib.ez — module name comes from directory
private const MAX_ITERATIONS int = 1000
private do validate(n int) -> bool {
return n > 0
}
do factorial(n int) -> int {
// Can call private members within the same module
if !validate(n) { return 1 }
// ...
}
Private members cannot be accessed from other modules:
import "./mathlib"
mathlib.factorial(5) // OK - public
// mathlib.validate(5) // ERROR E4006 - private function
// mathlib.MAX_ITERATIONS // ERROR E6009 - private constant
Attributes are annotations prefixed with # that modify declaration behavior. Attributes are placed on the line(s) immediately before a declaration, one attribute per line:
#doc("A person with a name and age")
#json
const Person struct {
name string
age int
}
Rules:
- One attribute per line, stacked before the declaration. Same-line multi-attribute (
#doc("x") #json) is not supported. - Order is irrelevant.
#docthen#jsonand#jsonthen#docproduce identical results. - Blank lines between attributes and the declaration are allowed.
- Each attribute applies to the immediately following declaration only. It does not skip ahead to find a compatible declaration further down the file.
- Misapplied attributes are rejected. For example,
#jsonon a function produces an error —#jsoncan only be applied to struct declarations.
| Attribute | Applies To | Description |
|---|---|---|
#doc("...") |
functions, structs, enums | Documentation metadata, used by ez doc |
#json |
structs | Enables JSON serialization for the struct |
#flags |
enums | Marks enum as a bitflag set (values are powers of 2) |
#strict |
when blocks |
Requires all enum variants to be handled |
The #doc attribute adds documentation metadata to functions, structs, and enums. Used by the ez doc command to generate documentation.
#doc("Adds two integers and returns the sum")
do add(a int, b int) -> int {
return a + b
}
#doc("Represents a 2D point")
const Point struct {
x int
y int
}
The #json attribute marks a struct for JSON serialization and deserialization. The compiler generates all marshaling and unmarshaling code automatically — no field tags, no manual encoding/decoding calls, and no error juggling at every step. Just annotate the struct and use json.parse() / json.stringify().
import @json
#json
const User struct {
name string
age int
active bool
}
do main() {
// Parse a JSON string directly into a typed struct
mut u User = json.parse("{\"name\": \"Alice\", \"age\": 25, \"active\": true}")
println(u.name) // Alice
// Serialize back to JSON — fields are mapped automatically
println(json.stringify(u)) // {"name":"Alice","age":25,"active":true}
}
json.parse() returns a fully typed struct (or array of structs), and json.stringify() accepts any #json struct and returns a string. The compiler knows the struct layout at compile time, so it generates field-by-field serialization code directly — there is no reflection, no runtime schema lookup, and no intermediate map step.
Rules:
- Field names in the JSON must match the struct field names exactly.
- Without
#json, the struct has no serialization machinery andjson.parse()/json.stringify()will fail. - Supported field types:
int,uint,float,string,bool. Nested#jsonstructs and arrays of#jsonstructs are also supported.
Functions can be passed as values using the () prefix syntax or the ref() builtin:
do is_positive(n int) -> bool { return n > 0 }
// ()func_name — implicit syntax
mut check = ()is_positive
// ref(func_name) — explicit syntax
mut check = ref(is_positive)
// Call through the reference
check(5) // true
// Pass as argument
do filter(arr [int], test func) -> [int] { ... }
mut positives = filter(numbers, ()is_positive)
Function references:
()func_nameis the implicit form (shorter)ref(func_name)is the explicit form (more readable)- Both produce identical results
- No anonymous functions or lambdas — every reference points to a named function
- The
functype is used for parameters that accept function references - References work with top-level and struct-namespaced functions
Functions can be declared inside struct blocks as namespaced free functions:
const Point struct {
x int
y int
do create(x int, y int) -> Point {
return Point{x: x, y: y}
}
do distance(a Point, b Point) -> float {
return math.sqrt(math.pow(float(a.x - b.x), 2) + math.pow(float(a.y - b.y), 2))
}
private do validate(p Point) -> bool {
return p.x >= 0 && p.y >= 0
}
}
// Called as Type.func()
mut p = Point.create(3, 4)
mut d = Point.distance(p1, p2)
Rules:
- No implicit
selforthis— every parameter is explicit privaterestricts access to other functions in the same struct- Called as
StructName.func_name(args...) - Cross-module:
module.StructName.func_name(args...) - Module-qualified types can be used in variable declarations, parameters, and return types:
mut p module.Point
When a struct function takes the struct (or a pointer to it) as its first parameter, callers can use the instance form instance.func(...) instead of writing the type name. The compiler rewrites the call as Type.func(instance, ...):
const Vec struct {
x int
y int
do len_sq(v Vec) -> int {
return v.x * v.x + v.y * v.y
}
do bump(&v Vec) {
v.x = v.x + 1
v.y = v.y + 1
}
}
mut a Vec = Vec{x: 3, y: 4}
a.len_sq() // sugar for Vec.len_sq(a)
Vec.len_sq(a) // still valid
a.bump() // sugar for Vec.bump(a); '&v' makes it a mutable alias
Both do f(v Vec) and do f(&v Vec) (mutable receiver) and do f(v ^Vec) (pointer receiver) participate in instance dispatch. The mutable-receiver form (&v) takes the instance by reference and may modify the caller's variable.
Factory-style functions whose first parameter isn't the struct (e.g. do make(x int) -> Vec) keep requiring the type-namespaced form (Vec.make(...)) — there is no instance to bind.
Chained struct function calls (a.f().g()) are not supported (E3075). Assign each intermediate result to a variable.
All functions in EZ are declared at the top level or inside struct blocks. Nested function declarations inside other functions are not permitted. Anonymous functions (lambdas/closures) are not supported.
The ? type is a wildcard placeholder that enables generic-style functions. When used in a function's parameter types, ? is bound to the concrete type of the argument at each call site. The return type can also use ? to propagate the bound type.
do identity(x ?) -> ? {
return x
}
mut a = identity(42) // ? binds to int, returns int
mut b = identity("hello") // ? binds to string, returns string
All ? placeholders in a function signature bind to the same concrete type:
do pick_first(a ?, b ?) -> ? {
return a
}
pick_first(1, 2) // OK — both args are int, ? binds to int
pick_first(1, "hello") // Error — conflicting bindings for ?
Wildcard types also work with composite types in parameters and returns:
do first(arr [?]) -> ? {
return arr[0]
}
mut x = first({1, 2, 3}) // ? binds to int
mut y = first({"a", "b"}) // ? binds to string
? is only valid in function parameter types and return types. It is rejected everywhere else:
| Usage | Result |
|---|---|
| Function parameter type | Allowed |
| Function return type | Allowed (must have at least one ? parameter) |
Variable declaration (mut x ?) |
Rejected (E2002) |
| Struct field type | Rejected (E2070) |
Array type in variable ([?]) |
Rejected (E2070) |
Map type in variable (map[string:?]) |
Rejected (E2070) |
new(?) |
Rejected |
Named return type (-> (name ?)) |
Rejected (E3082) — use unnamed -> (?) instead |
- The concrete type is inferred from the first argument that corresponds to a
?parameter - All subsequent
?parameters and the return type must be consistent with that binding - If the return type uses
?, at least one parameter must also use?to provide the binding
Module identity is determined by the filesystem — there are no module declarations. A file's module name is its filename minus the .ez extension. A directory's module name is its directory name.
project/
main.ez ← entry point
helpers.ez ← module "helpers"
utils.ez ← module "utils"
models/
user.ez ← ┐
task.ez ← ┘ merge into module "models"
internal/
cache.ez ← separate module "internal"
Directory imports merge all top-level .ez files in that directory into a single namespace under the directory's name. Subdirectories are not included — they are separate modules that must be imported independently. Hidden files (names starting with .) are excluded from directory scans.
All relative import paths are resolved relative to the entry point file's directory, not the importing file's directory. This means a file inside models/ that uses import "./shared.ez" resolves relative to the project root (where main.ez lives), not relative to models/.
import_decl = "import" [ "and" "use" ] import_path { "," import_path } .
import_path = [ alias ] ( "@" identifier | string_literal ) | "c" string_literal .
Standard library imports are prefixed with @:
import @arrays, @maps, @strings
Local imports use relative string paths. The compiler resolves them in order:
- If the path ends in
.ez, import that file directly. - If the path has no extension, try appending
.ez— if a file exists, import it. - If the path (without extension) is a directory, scan it for all
.ezfiles and merge them into one module. - If none of the above match, emit E6002.
import "./helpers.ez" // explicit file import
import "./helpers" // resolves to helpers.ez (file) or helpers/ (directory)
import "./models" // models/ directory — all .ez files merge
When both helpers.ez and a helpers/ directory exist, the file takes priority.
If a directory contains no .ez files, it is an error (E6003).
Import aliasing:
import m @math // use m.sqrt() instead of math.sqrt()
import mymod "./server" // use mymod.handle() instead of server.handle()
Multiple imports can be comma-separated:
import @math, "./helpers", "./models"
Collision detection: If two different imports resolve to the same module name, it is an error (E6001). The user must alias one to disambiguate:
// Error: both resolve to module name "utils"
import "./utils", "./lib/utils"
// Fix: alias one
import "./utils", lib_utils "./lib/utils"
Directory import semantics:
When a directory is imported, all .ez files within it are merged into a single module namespace (the directory name). The following rules apply:
- Files within the directory may import each other via relative paths (e.g.,
import "./types.ez"). These sibling cross-references are resolved internally and do not create separate namespaces — all symbols remain under the directory's namespace. - Transitive imports inside directory files resolve relative to the importing file's location, not the entry file.
- If a file inside a directory imports its own parent directory (self-referential import), it is rejected with E6004.
- If a directory import is followed by a direct import of a file already in that directory, the compiler emits W2015 indicating the import is redundant — the directory namespace should be used instead.
- If two files in a directory declare the same symbol name, it is a collision error (E4004).
Deduplication: If the same file is imported multiple times (e.g., directly by main and transitively through another import), it is only processed once. No error is emitted.
The import and use syntax combines importing and using in a single statement:
import and use @arrays
This is equivalent to:
import @arrays
using arrays
Multiple modules can be combined:
import and use @arrays, @strings
The using declaration brings module members into scope for unqualified access. It can be placed at file scope or function scope:
File scope — all functions in the file can use unqualified access:
import @strings
using strings
do main() {
println(to_upper("hello")) // OK — strings is in file scope
}
do shout(s string) -> string {
return to_upper(s) // OK — same file scope
}
Function scope — only that function can use unqualified access:
import @strings
do main() {
using strings
println(to_upper("hello")) // OK — strings is in scope here
}
do shout(s string) -> string {
return strings.to_upper(s) // must qualify — using is not in scope here
}
Multiple modules can be listed:
using arrays, strings
Without using, module members are accessed with dot notation:
import @math
math.sqrt(16.0)
With using:
import @math
using math
sqrt(16.0)
EZ can import C headers and call C functions directly using the c prefix:
c_import = "import" "c" string_literal .
import c"stdio.h" // system header → #include <stdio.h>
import c"./mylib.h" // local header → #include "./mylib.h"
System headers (no ./ or ../ prefix) emit angle-bracket includes. Local headers emit quoted includes. Multiple C imports can be comma-separated:
import c"stdio.h", c"stdlib.h", c"string.h"
C imports can be mixed with EZ imports on separate lines:
import @math
import c"stdio.h"
All C functions are accessed via the c. prefix:
import c"stdio.h"
do main() {
c.puts("hello from C")
c.printf("value: %d\n", 42)
}
C constants and macros are accessed with the same c. prefix:
import c"stdio.h"
do main() {
println(c.EOF) // -1
println(c.EXIT_SUCCESS) // 0
}
| EZ type | C type | Notes |
|---|---|---|
int |
int64_t |
Use i32 for C int |
uint |
uint64_t |
Use u32 for C unsigned int |
i8, i16, i32, i64 |
int8_t, int16_t, int32_t, int64_t |
Exact match |
u8, u16, u32, u64 |
uint8_t, uint16_t, uint32_t, uint64_t |
Exact match |
float |
double |
Use f32 for C float |
bool |
bool |
Exact match |
byte |
uint8_t |
Exact match |
char |
int32_t |
EZ uses 32-bit for Unicode |
string |
char* |
Auto-converted when passed to C functions |
^T |
T* |
Direct pointer mapping |
String conversion: EZ strings are automatically converted to char* when passed to C functions. To convert a C char* return value back to an EZ string, use the c_string() builtin:
import c"stdlib.h"
do main() {
mut home string = c_string(c.getenv("HOME"))
println(home)
}
Return types: C function return types are inferred by the C compiler. If EZ needs to know the type (e.g., for println), add a type annotation:
import c"math.h"
do main() {
mut x float = c.sqrt(2.0) // type annotation needed
println(x) // prints 1.4142135623730951
}
The following EZ types cannot be passed to C functions:
i128,i256,u128,u256— C has no 128/256-bit integer types- Arrays and maps — EZ-specific types with no C equivalent
- EZ structs — pass individual fields instead
C structs returned from C functions can be passed back to other C functions via __auto_type inference.
The module name c is reserved for C interop. Files named c.ez must use an explicit alias:
import myc "./c.ez" // OK — aliased
import "./c.ez" // Error — 'c' is reserved
The EZ standard library consists of 26 modules plus built-in functions that require no import.
Built-in functions are always available without importing any module.
| Function | Signature | Description |
|---|---|---|
println |
(value T) |
Print value with newline. Accepts any type. |
print |
(value T) |
Print value without newline. Accepts any type. |
eprintln |
(value T) |
Print to stderr with newline. Accepts any type. |
eprint |
(value T) |
Print to stderr without newline. Accepts any type. |
All types are printable: string, int, float, bool, arrays, maps, structs, and pointers.
| Function | Signature | Description |
|---|---|---|
input |
() -> string |
Read line from stdin |
| Function | Description |
|---|---|
i128, i256 |
Convert to wide signed integers (struct-based, 128/256-bit) |
u128, u256 |
Convert to wide unsigned integers (struct-based, 128/256-bit) |
| Function | Signature | Description |
|---|---|---|
len |
(collection) -> int |
Length of array, map, or string (byte length for strings, not character count) |
type_of |
(value T) -> string |
Returns the EZ type name as a string (e.g. "int", "uint", "float", "string", "i128", "u256"). Accepts any type. |
size_of |
(Type) -> int |
Size of type in bytes |
copy |
(value T) -> T |
Create deep copy. Accepts any type. |
new |
(Type) -> ^Type |
Allocate zero-initialized struct on arena |
ref |
(variable T) -> ref<T> |
Create a transparent reference (alias) to a variable. Reads and writes through the reference affect the original. Mutability is determined by the declaration (mut or const). |
addr |
(variable) -> ^T |
Get memory address of a variable |
error |
(message string) -> Error |
Create error value |
assert |
(condition bool, message string) |
Assert condition is true |
panic |
(message string) |
Terminate with error message |
exit |
(code int) |
Exit program with code |
range |
(start int, end int [, step int]) -> Range |
Create integer range |
cast |
(value T, Type) -> Type |
Explicit type conversion |
to_char |
(s string, index int) -> int |
Return the Unicode codepoint at character position index (not byte position). Panics if index is out of bounds. |
char_count |
(s string) -> int |
Return the number of Unicode characters (codepoints) in a string. Unlike len(), which returns byte count, char_count() counts decoded UTF-8 characters. |
c_string |
(ptr ^u8) -> string |
Convert a C char* return value to an EZ string (for C interop) |
Reference behavior with ref():
The ref() function creates a reference to an existing value. The mutability of the reference depends on the variable declaration:
mut arr [int] = {1, 2, 3}
// mut ref is mutable - can modify through the reference
mut r1 = ref(arr)
arrays.append(r1, 4) // OK - modifies arr
// const ref is read-only - can read but not modify
const r2 = ref(arr)
mut val = r2[0] // OK - can read
arrays.append(r2, 5) // ERROR - cannot modify through const ref
// const ref sees changes made to the original
arrays.append(arr, 6)
println(r2[4]) // Prints 6 - r2 sees the change
Mutability rules:
| Reference declaration | Source | Allowed? |
|---|---|---|
mut r = ref(x) |
mut |
yes |
const r = ref(x) |
mut |
yes — read-only view of a mutable source |
const r = ref(x) |
const |
yes |
mut r = ref(x) |
const |
no — E3079; you cannot get a mutable reference to a const source. Use copy(x) to obtain an independent mutable instance. |
Argument requirement: ref() requires a variable, struct field, array index, or pointer dereference — anything with a stable address. Literals, call results, and arithmetic expressions are rejected (E3012). The same rule applies to addr(), and the check recurses through member/index chains, so ref(some_call().field) and addr(arr[0]) are validated end-to-end.
| Function | Signature | Description |
|---|---|---|
sleep_s |
(seconds int) |
Sleep for seconds |
sleep_ms |
(ms int) |
Sleep for milliseconds |
sleep_ns |
(ns int) |
Sleep for nanoseconds |
| Function | Signature | Description |
|---|---|---|
is_empty |
(arr [T]) -> bool |
Check if array is empty |
contains |
(arr [T], value T) -> bool |
Check if value exists |
index_of |
(arr [T], value T) -> int |
First index of value (-1 if not found) |
count |
(arr [T], value T) -> int |
Count occurrences of value |
is_equal |
(a [T], b [T]) -> bool |
Structural equality. Compares length first, then elements. T must be a primitive (int, uint, float, bool, char, byte, sized variants) or string; arrays of nested composites are rejected at compile time. |
The == and != operators on arrays are not allowed (E3074); use arrays.is_equal(a, b) for equality.
| Function | Signature | Description |
|---|---|---|
get_first |
(arr [T]) -> T |
Return first element (panic if empty) |
get_last |
(arr [T]) -> T |
Return last element (panic if empty) |
| Function | Signature | Description |
|---|---|---|
append |
(&arr [T], value T) |
Append element |
prepend |
(&arr [T], value T) |
Insert value at front |
insert_at |
(&arr [T], index int, value T) |
Insert at index |
remove |
(&arr [T], value T) |
Remove first occurrence of value |
remove_at |
(&arr [T], index int) |
Remove element at index |
remove_first |
(&arr [T]) -> T |
Remove and return first element (panic if empty) |
remove_last |
(&arr [T]) -> T |
Remove and return last element (panic if empty) |
clear |
(&arr [T]) |
Remove all elements |
fill |
(&arr [T], value T, count int) |
Fill array with N copies of value |
sort_asc |
(&arr [T]) |
Sort ascending in-place |
sort_desc |
(&arr [T]) |
Sort descending in-place |
| Function | Signature | Description |
|---|---|---|
reverse |
(arr [T]) -> [T] |
Return reversed copy |
slice |
(arr [T], start int, end int) -> [T] |
Return slice |
concat |
(a [T], b [T]) -> [T] |
Concatenate two arrays |
deduplicate |
(arr [T]) -> [T] |
Remove duplicate values |
flatten |
(arr [[T]]) -> [T] |
Flatten one level of nesting |
split_every |
(arr [T], size int) -> [[T]] |
Split into sub-arrays of given size |
pair |
(a [T], b [T]) -> [[T]] |
Pair elements from two arrays |
| Function | Signature | Description |
|---|---|---|
get_sum |
(arr [T]) -> T |
Sum all elements. Accepts int, float, or any sized integer/float type. |
get_min |
(arr [T]) -> T |
Minimum element |
get_max |
(arr [T]) -> T |
Maximum element |
| Function | Signature | Description |
|---|---|---|
to_upper |
(s string) -> string |
Convert to uppercase |
to_lower |
(s string) -> string |
Convert to lowercase |
| Function | Signature | Description |
|---|---|---|
is_empty |
(s string) -> bool |
Check if empty (after trim) |
contains |
(s string, sub string) -> bool |
Check if contains substring |
starts_with |
(s string, prefix string) -> bool |
Check prefix |
ends_with |
(s string, suffix string) -> bool |
Check suffix |
index_of |
(s string, sub string) -> int |
First index of substring |
count |
(s string, sub string) -> int |
Count occurrences |
| Function | Signature | Description |
|---|---|---|
trim |
(s string) -> string |
Trim whitespace |
trim_left |
(s string) -> string |
Trim left whitespace |
trim_right |
(s string) -> string |
Trim right whitespace |
replace |
(s string, old string, new string) -> string |
Replace all occurrences |
repeat |
(s string, count int) -> string |
Repeat string |
reverse |
(s string) -> string |
Reverse string |
| Function | Signature | Description |
|---|---|---|
split |
(s string, sep string) -> [string] |
Split into array |
join |
(arr [string], sep string) -> string |
Join array |
slice |
(s string, start int, end int) -> string |
Extract substring |
| Function | Signature | Description |
|---|---|---|
is_empty |
(m map[K:V]) -> bool |
Check if map is empty |
has_key |
(m map[K:V], key K) -> bool |
Check if key exists |
contains_value |
(m map[K:V], value V) -> bool |
Check if any entry has the given value |
get_keys |
(m map[K:V]) -> [K] |
Get all keys as an array |
get_values |
(m map[K:V]) -> [V] |
Get all values as an array |
get_or_default |
(m map[K:V], key K, default V) -> V |
Get value or return default if key missing |
remove_key |
(&m map[K:V], key K) -> bool |
Remove key-value pair |
clear |
(&m map[K:V]) |
Remove all entries |
merge |
(m1 map[K:V], m2 map[K:V]) -> map[K:V] |
Combine two maps (m2 overwrites on conflict) |
is_equal |
(a map[K:V], b map[K:V]) -> bool |
Structural equality. Compares counts, then iterates the first map's entries in insertion order looking each key up in the second. K and V must each be a primitive (int, uint, float, bool, char, byte, sized variants) or string; maps with nested-composite values are rejected at compile time. |
The == and != operators on maps are not allowed (E3076); use maps.is_equal(a, b) for equality. Maps have no defined ordering, so < / <= / > / >= on maps is also rejected.
Use len(m) to get the number of entries (builtin, no import needed).
Unless noted otherwise, all math functions accept int, float, and sized numeric types (i8–i64, u8–u64, f32, f64). Functions that return the same type as their input are marked with -> T below; others specify their return type explicitly.
| Function | Signature | Description |
|---|---|---|
abs |
(n T) -> T |
Absolute value |
neg |
(n T) -> T |
Negation |
sign |
(n T) -> int |
Sign (-1, 0, or 1) |
| Function | Signature | Description |
|---|---|---|
min |
(a T, b T) -> T |
Minimum of two values |
max |
(a T, b T) -> T |
Maximum of two values |
clamp |
(value T, min T, max T) -> T |
Clamp value to range [min, max] |
| Function | Signature | Description |
|---|---|---|
floor |
(n T) -> int |
Round down to nearest integer |
ceil |
(n T) -> int |
Round up to nearest integer |
round |
(n T) -> int |
Round to nearest integer |
trunc |
(n T) -> int |
Truncate toward zero |
| Function | Signature | Description |
|---|---|---|
pow |
(base T, exp T) -> float |
Raise base to exponent |
sqrt |
(n T) -> float |
Square root |
cbrt |
(n T) -> float |
Cube root |
hypot |
(x T, y T) -> float |
Hypotenuse (sqrt(x^2 + y^2)) |
exp |
(n T) -> float |
e raised to the power n |
exp2 |
(n T) -> float |
2 raised to the power n |
| Function | Signature | Description |
|---|---|---|
log |
(n T) -> float |
Natural logarithm |
log2 |
(n T) -> float |
Base 2 logarithm |
log10 |
(n T) -> float |
Base 10 logarithm |
log_base |
(value T, base T) -> float |
Logarithm with custom base |
| Function | Signature | Description |
|---|---|---|
sin |
(rad T) -> float |
Sine |
cos |
(rad T) -> float |
Cosine |
tan |
(rad T) -> float |
Tangent |
asin |
(n T) -> float |
Arc sine |
acos |
(n T) -> float |
Arc cosine |
atan |
(n T) -> float |
Arc tangent |
atan2 |
(y T, x T) -> float |
Arc tangent of y/x |
sinh |
(n T) -> float |
Hyperbolic sine |
cosh |
(n T) -> float |
Hyperbolic cosine |
tanh |
(n T) -> float |
Hyperbolic tangent |
deg_to_rad |
(deg T) -> float |
Degrees to radians |
rad_to_deg |
(rad T) -> float |
Radians to degrees |
| Function | Signature | Description |
|---|---|---|
factorial |
(n int) -> int |
Factorial (n must be non-negative) |
gcd |
(a int, b int) -> int |
Greatest common divisor |
lcm |
(a int, b int) -> int |
Least common multiple |
| Function | Signature | Description |
|---|---|---|
is_prime |
(n int) -> bool |
Check if prime |
is_even |
(n int) -> bool |
Check if even |
is_odd |
(n int) -> bool |
Check if odd |
is_infinite |
(n float) -> bool |
Check if infinite |
is_nan |
(n float) -> bool |
Check if NaN |
is_finite |
(n float) -> bool |
Check if finite (not infinite or NaN) |
| Function | Signature | Description |
|---|---|---|
lerp |
(a T, b T, t T) -> float |
Linear interpolation between a and b by factor t |
distance |
(x1 T, y1 T, x2 T, y2 T) -> float |
Euclidean distance between two 2D points |
PI- Pi (3.14159...)E- Euler's number (2.71828...)PHI- Golden ratio (1.61803...)SQRT2- Square root of 2LN2- Natural log of 2LN10- Natural log of 10TAU- Tau (2*Pi)INF- Positive infinityNEG_INF- Negative infinityEPSILON- Smallest representable float difference
| Function | Signature | Description |
|---|---|---|
now |
() -> int |
Current Unix timestamp (seconds) |
now_ms |
() -> int |
Current Unix timestamp (milliseconds) |
now_ns |
() -> int |
Current Unix timestamp (nanoseconds) |
| Function | Signature | Description |
|---|---|---|
year |
(timestamp int) -> int |
Get year |
month |
(timestamp int) -> int |
Get month (1-12) |
day |
(timestamp int) -> int |
Get day of month |
hour |
(timestamp int) -> int |
Get hour |
minute |
(timestamp int) -> int |
Get minute |
second |
(timestamp int) -> int |
Get second |
weekday |
(timestamp int) -> int |
Get day of week (0=Sunday) |
| Function | Signature | Description |
|---|---|---|
format |
(format string, timestamp int) -> string |
Format time |
to_iso |
(timestamp int) -> string |
ISO 8601 string |
date |
(timestamp int) -> string |
Date (YYYY-MM-DD) |
to_clock |
(timestamp int) -> string |
Time (HH:MM:SS) |
| Function | Signature | Description |
|---|---|---|
tick |
() -> int |
High-resolution timestamp in nanoseconds |
elapsed_ms |
(start_tick int) -> int |
Milliseconds elapsed since a tick |
Some random functions accept a variable number of arguments (e.g., rand_int with 1 or 2 args). This is not general function overloading — it is special-case codegen dispatching within the stdlib only.
| Function | Signature | Description |
|---|---|---|
rand_float |
() -> float |
Random float [0.0, 1.0) |
rand_float |
(min float, max float) -> float |
Random float [min, max) |
rand_int |
(max int) -> int |
Random int [0, max) |
rand_int |
(min int, max int) -> int |
Random int [min, max) |
rand_bool |
() -> bool |
Random boolean |
rand_byte |
() -> byte |
Random byte [0, 255] |
rand_char |
() -> char |
Random printable char |
rand_char |
(min char, max char) -> char |
Random char in range |
random_hex |
(length int) -> string |
Cryptographically secure random hex |
choice |
(arr [T]) -> T |
Random element from array |
shuffle |
(arr [T]) -> [T] |
Return shuffled copy |
sample |
(arr [T], n int) -> [T] |
Return n unique random elements |
seed |
(value int) |
Seed the random number generator |
| Function | Signature | Description |
|---|---|---|
decode |
(text string) -> map[string:string] |
Decode JSON string to map |
parse |
(text string) -> T |
Parse JSON into a #json struct (context-dependent) |
encode |
(value T) -> string |
Encode to JSON string. Accepts int, float, bool, string, map, array. |
stringify |
(value T) -> string |
Encode to JSON string (alias for encode, supports #json structs) |
format |
(value T) -> string |
Format value as JSON string |
pretty_print |
(m map, indent int) -> string |
Pretty-print a map as indented JSON |
is_valid |
(text string) -> bool |
Check if valid JSON |
Error-returning variant: decode
| Function | Signature | Description |
|---|---|---|
read_file |
(path string) -> string |
Read file as string |
| Function | Signature | Description |
|---|---|---|
write_file |
(path string, content string) -> bool |
Write file |
append_file |
(path string, content string) -> bool |
Append to file |
| Function | Signature | Description |
|---|---|---|
file_exists |
(path string) -> bool |
Check if file exists |
is_file |
(path string) -> bool |
Check if path is file |
is_directory |
(path string) -> bool |
Check if path is directory |
file_size |
(path string) -> int |
Get file size in bytes |
delete_file |
(path string) -> bool |
Delete file |
rename_file |
(old_path string, new_path string) -> bool |
Rename file |
copy_file |
(src string, dst string) -> bool |
Copy file |
move_file |
(src string, dst string) -> bool |
Move file |
| Function | Signature | Description |
|---|---|---|
list_dir |
(path string) -> [string] |
List directory entries |
make_dir |
(path string) -> bool |
Create directory |
make_dir_all |
(path string) -> bool |
Create directory and all parents |
remove_dir |
(path string) -> bool |
Remove empty directory |
remove_dir_all |
(path string) -> bool |
Remove directory and all contents |
walk |
(path string) -> [string] |
Recursively list all files |
glob |
(pattern string) -> [string] |
Match files by glob pattern |
| Function | Signature | Description |
|---|---|---|
path_join |
(a string, b string) -> string |
Join two path components |
dirname |
(path string) -> string |
Parent directory of path |
basename |
(path string) -> string |
Filename component of path |
extension |
(path string) -> string |
File extension (including dot) |
is_absolute |
(path string) -> bool |
Check if path is absolute |
normalize |
(path string) -> string |
Clean and normalize path |
Error-returning variants: read_file, write_file, delete_file, append_file, rename_file, copy_file, move_file, list_dir, make_dir, make_dir_all, remove_dir, remove_dir_all, walk
All relative paths passed to @io functions (and any other stdlib function that accepts a file path, including csv.read_file, csv.write_file, and sqlite.open) are resolved relative to the current working directory of the process — the directory from which the program was launched. They are not resolved relative to the source file that contains the call.
// Given project layout:
// project/
// main.ez
// src/cli.ez
// data/config.json
// Running from project/: ez main.ez
// All of these resolve from project/, regardless of which .ez file calls them:
io.read_file("data/config.json") // project/data/config.json
io.read_file("./data/config.json") // same thing
io.read_file("/etc/hosts") // absolute path — unaffected by cwd
| Function | Signature | Description |
|---|---|---|
get_env |
(name string) -> string |
Get environment variable |
set_env |
(name string, value string) |
Set environment variable |
| Function | Signature | Description |
|---|---|---|
args |
() -> [string] |
Get command-line arguments |
current_dir |
() -> string |
Get current working directory |
hostname |
() -> string |
Get machine hostname |
pid |
() -> int |
Get process ID |
current_os |
() -> int |
Get current OS as an int matching the constants below (MAC_OS, LINUX, WINDOWS, OTHER) |
arch |
() -> string |
Get CPU architecture |
MAC_OS= 0LINUX= 1WINDOWS= 2OTHER= 3
HTTP client for making requests. Currently supports HTTP only.
| Function | Signature | Description |
|---|---|---|
get |
(url string) -> HttpResponse |
GET request |
post |
(url string, body string) -> HttpResponse |
POST request |
put |
(url string, body string) -> HttpResponse |
PUT request |
patch |
(url string, body string) -> HttpResponse |
PATCH request |
delete |
(url string) -> HttpResponse |
DELETE request |
head |
(url string) -> HttpResponse |
HEAD request |
Error-returning variants: get, post, put, delete, head, patch
The HttpResponse struct is available when either @http or @server is imported.
status int- HTTP status codebody string- Response bodyheaders map- Response headers
| Function | Signature | Description |
|---|---|---|
sha256 |
(data string) -> string |
SHA-256 hash (hex) |
md5 |
(data string) -> string |
MD5 hash (hex) |
random_hex |
(length int) -> string |
Cryptographically secure random hex string |
| Function | Signature | Description |
|---|---|---|
base64_encode |
(s string) -> string |
Encode to base64 |
base64_decode |
(s string) -> string |
Decode from base64 |
hex_encode |
(s string) -> string |
Encode to hex |
hex_decode |
(s string) -> string |
Decode from hex |
url_encode |
(s string) -> string |
URL percent-encode |
url_decode |
(s string) -> string |
URL percent-decode |
| Function | Signature | Description |
|---|---|---|
generate |
() -> string |
Generate UUID v4 without hyphens |
generate_hyphenated |
() -> string |
Generate UUID v4 with hyphens |
is_valid |
(s string) -> bool |
Validate UUID format |
| Function | Signature | Description |
|---|---|---|
from_string |
(s string) -> [byte] |
Create from UTF-8 string |
from_hex |
(hex string) -> [byte] |
Decode hex string |
from_base64 |
(b64 string) -> [byte] |
Decode base64 string |
to_string |
(bytes [byte]) -> string |
Convert to UTF-8 string |
to_hex |
(bytes [byte]) -> string |
Encode to hex string |
to_base64 |
(bytes [byte]) -> string |
Encode to base64 string |
Binary encoding/decoding for integers and floats in little-endian (le) and big-endian (be) formats.
| Function | Description |
|---|---|
encode_i8, decode_i8 |
Signed 8-bit |
encode_u8, decode_u8 |
Unsigned 8-bit |
| Function | Description |
|---|---|
encode_i16_le, encode_i16_be, decode_i16_le, decode_i16_be |
Signed 16-bit |
encode_u16_le, encode_u16_be, decode_u16_le, decode_u16_be |
Unsigned 16-bit |
| Function | Description |
|---|---|
encode_i32_le, encode_i32_be, decode_i32_le, decode_i32_be |
Signed 32-bit |
encode_u32_le, encode_u32_be, decode_u32_le, decode_u32_be |
Unsigned 32-bit |
| Function | Description |
|---|---|
encode_i64_le, encode_i64_be, decode_i64_le, decode_i64_be |
Signed 64-bit |
encode_u64_le, encode_u64_be, decode_u64_le, decode_u64_be |
Unsigned 64-bit |
| Function | Description |
|---|---|
encode_i128_le, encode_i128_be, decode_i128_le, decode_i128_be |
Signed 128-bit |
encode_u128_le, encode_u128_be, decode_u128_le, decode_u128_be |
Unsigned 128-bit |
| Function | Description |
|---|---|
encode_i256_le, encode_i256_be, decode_i256_le, decode_i256_be |
Signed 256-bit |
encode_u256_le, encode_u256_be, decode_u256_le, decode_u256_be |
Unsigned 256-bit |
| Function | Description |
|---|---|
encode_f32_le, encode_f32_be, decode_f32_le, decode_f32_be |
32-bit float |
encode_f64_le, encode_f64_be, decode_f64_le, decode_f64_be |
64-bit float |
SQLite database access for persistent storage.
| Function | Signature | Description |
|---|---|---|
open |
(path string) -> Database |
Open or create a SQLite database |
close |
(db Database) |
Close database connection |
exec |
(db Database, sql string, ...params string) |
Execute a SQL statement with optional parameters |
query |
(db Database, sql string, ...params string) -> [map[string:string]] |
Execute a parameterized SQL query, returns array of row maps |
Error-returning variants: open, exec, query
Parameterized queries use ? placeholders to prevent SQL injection:
sqlite.exec(db, "INSERT INTO users VALUES (?, ?)", name, age)
mut rows = sqlite.query(db, "SELECT * FROM users WHERE age > ?", 18)
An HTTP server module with dynamic handlers and path parameters.
| Function | Signature | Description |
|---|---|---|
add_router |
() -> Router |
Create a new router |
add_route |
(router Router, method string, path string, ()handler) |
Add a route with handler function |
listen |
(port int, router Router) |
Start HTTP server on port (blocks until killed) |
cors |
(router Router, origin string) |
Enable CORS with the given origin |
use |
(router Router, ()middleware) |
Register a middleware function |
| Function | Signature | Description |
|---|---|---|
text |
(status int, body string) -> HttpResponse |
Create text/plain response |
json |
(status int, data) -> HttpResponse |
Create application/json response |
html |
(status int, body string) -> HttpResponse |
Create text/html response |
redirect |
(status int, url string) -> HttpResponse |
Create redirect response |
The HttpRequest type is only available when @server is imported. Importing @http alone does not provide this type.
Every handler receives an HttpRequest struct:
| Field | Type | Description |
|---|---|---|
method |
string |
HTTP method (GET, POST, etc.) |
path |
string |
Request path |
query |
map[string:string] |
Query parameters |
headers |
map[string:string] |
Request headers |
params |
map[string:string] |
Path parameters (from :param segments) |
body |
string |
Raw request body |
import @server
do home(req HttpRequest) -> HttpResponse {
return server.text(200, "Welcome!")
}
do get_user(req HttpRequest) -> HttpResponse {
mut id = req.params["id"]
return server.json(200, {"id": id})
}
do main() {
mut r = server.add_router()
server.cors(r, "*")
server.add_route(r, "GET", "/", ()home)
server.add_route(r, "GET", "/users/:id", ()get_user)
server.listen(8080, r)
}
Regular expression operations using POSIX extended regex syntax.
| Function | Signature | Description |
|---|---|---|
is_valid |
(pattern string) -> bool |
Check if pattern is valid regex |
is_match |
(pattern string, text string) -> bool |
Check if pattern matches text |
find |
(pattern string, text string) -> string |
First match |
find_all |
(pattern string, text string) -> [string] |
All matches |
replace |
(pattern string, text string, replacement string) -> string |
Replace matches |
split |
(pattern string, text string) -> [string] |
Split by pattern |
Error-returning variants: find, find_all, replace, split
Reading and writing CSV (Comma-Separated Values) data.
| Function | Signature | Description |
|---|---|---|
parse |
(csv_string string) -> [[string]] |
Parse CSV string to 2D array |
decode |
(csv_string string) -> [[string]] |
Parse CSV string to 2D array (alias for parse) |
encode |
(data [[string]]) -> string |
Encode 2D array to CSV string |
format |
(data [[string]]) -> string |
Format 2D array as CSV string (alias for encode) |
read_file |
(path string) -> [[string]] |
Read and parse CSV file |
write_file |
(path string, data [[string]]) -> bool |
Write 2D array to CSV file |
headers |
(data [[string]]) -> [string] |
Extract header row from parsed CSV data |
Error-returning variants: read_file, write_file
TCP sockets and DNS resolution.
| Function | Signature | Description |
|---|---|---|
connect |
(host string, port int) -> Socket |
Connect to a remote host |
listen |
(port int) -> Listener |
Listen for incoming connections on a port |
accept |
(listener Listener) -> Socket |
Accept an incoming connection |
send |
(sock Socket, data string) -> int |
Send data over a socket, returns bytes sent |
receive |
(sock Socket, max_bytes int) -> string |
Receive up to max_bytes bytes from a socket |
close |
(sock Socket) |
Close a socket or listener |
set_timeout |
(sock Socket, ms int) |
Set read/write timeout in milliseconds |
resolve |
(hostname string) -> string |
Resolve a hostname to an IP address |
Error-returning variants: connect, listen, accept, send, receive, resolve
Thread lifecycle management. Compiler-only feature; requires POSIX threads.
| Function | Signature | Description |
|---|---|---|
spawn |
(()func) -> Thread |
Spawn a new thread running func |
spawn |
(()func, arg int) -> Thread |
Spawn a new thread running func with an int argument |
join |
(t Thread) |
Wait for a thread to finish |
get_id |
() -> int |
Get the current thread's ID |
Synchronization primitives for thread-safe access to shared data. Compiler-only feature; requires POSIX threads.
| Function | Signature | Description |
|---|---|---|
mutex |
() -> Mutex |
Create a new mutex |
lock |
(m Mutex) |
Acquire a mutex |
unlock |
(m Mutex) |
Release a mutex |
try_lock |
(m Mutex) |
Try to acquire a mutex (non-blocking) |
destroy |
(m Mutex) |
Destroy a mutex |
Message passing between threads. Compiler-only feature; requires POSIX threads.
| Function | Signature | Description |
|---|---|---|
open |
(capacity int) -> Channel |
Create a buffered channel |
send |
(ch Channel, value) |
Send a value into a channel |
receive |
(ch Channel) -> T |
Receive a value from a channel |
close |
(ch Channel) |
Close a channel |
Arena-based memory allocation. Compiler-only feature.
| Function | Signature | Description |
|---|---|---|
arena |
(size int) -> Arena |
Create an arena with the given byte capacity |
destroy |
(arena Arena) |
Destroy an arena and free its memory |
reset |
(arena Arena) |
Reset an arena, reclaiming all allocations without freeing |
usage |
(arena Arena) -> int |
Return the number of bytes currently used |
init |
(arena Arena, Type) -> ^Type |
Allocate a zero-initialized value of Type in the arena |
alloc |
(arena Arena, value T) -> ^T |
Allocate a copy of value in the arena |
make |
(arena Arena, Type) -> ^Type |
Allocate zero-initialized value of Type in arena |
free |
(arena Arena) |
Free an arena (alias for destroy) |
size_of |
(Type) -> int |
Size of type in bytes |
raw_copy |
(dest ptr, src ptr, n int) |
Copy n bytes from src to dest |
zero |
(p ptr, n int) |
Zero out n bytes at p |
fill |
(p ptr, value int, n int) |
Set n bytes at p to value |
Lock-free atomic operations backed by hand-written assembly (ARM64 and x86_64). Compiler-only feature.
All pointer arguments must be ^int (pointer to int).
| Function | Signature | Description |
|---|---|---|
load |
(ptr ^int) -> int |
Atomically load a value |
store |
(ptr ^int, val int) |
Atomically store a value |
add |
(ptr ^int, val int) -> int |
Atomic add; returns previous value |
sub |
(ptr ^int, val int) -> int |
Atomic subtract; returns previous value |
exchange |
(ptr ^int, val int) -> int |
Atomic swap; returns previous value |
cas |
(ptr ^int, expected int, desired int) -> bool |
Compare-and-swap; returns true on success |
and |
(ptr ^int, val int) -> int |
Atomic bitwise AND; returns previous value |
or |
(ptr ^int, val int) -> int |
Atomic bitwise OR; returns previous value |
xor |
(ptr ^int, val int) -> int |
Atomic bitwise XOR; returns previous value |
| Function | Signature | Description |
|---|---|---|
spinlock |
() -> SpinLock |
Create a new spinlock |
spin_lock |
(lk SpinLock) |
Acquire a spinlock (spins until acquired) |
spin_trylock |
(lk SpinLock) -> bool |
Try to acquire; returns true if acquired |
spin_unlock |
(lk SpinLock) |
Release a spinlock |
| Function | Signature | Description |
|---|---|---|
fence |
() |
Full memory barrier (sequential consistency) |
Formatted output and string formatting functions.
| Function | Signature | Description |
|---|---|---|
printf |
(format string, ...args T) |
Print formatted string to stdout |
sprintf |
(format string, ...args T) -> string |
Return formatted string |
format |
(format string, ...args T) -> string |
Return formatted string |
eprintln |
(value T) |
Print value followed by newline to stderr |
eprint |
(s string) |
Print string to stderr (no newline) |
| Function | Signature | Description |
|---|---|---|
pad_left |
(s string, width int, ch char) -> string |
Pad string on the left to width with ch |
pad_right |
(s string, width int, ch char) -> string |
Pad string on the right to width with ch |
center |
(s string, width int, ch char) -> string |
Center string within width, padding both sides with ch |
| Function | Signature | Description |
|---|---|---|
int_to_hex |
(n int) -> string |
Format integer as lowercase hexadecimal (no 0x prefix) |
int_to_binary |
(n int) -> string |
Format integer as binary |
int_to_octal |
(n int) -> string |
Format integer as octal |
float_fixed |
(f float, decimals int) -> string |
Format float with fixed decimal places |
float_sci |
(f float) -> string |
Format float in scientific notation |
Accepts string, int, float, and bool arguments for formatted output functions. Composite types (structs, arrays, maps) are not supported and are rejected at compile time (E3017). Use println for printing composite types.
String-to-type and type-to-string conversion functions with proper error handling.
| Function | Signature | Description |
|---|---|---|
to_int |
(s string, base int = 10) -> (int, Error) |
Parse string as signed integer in given base |
to_uint |
(s string, base int = 10) -> (uint, Error) |
Parse string as unsigned integer in given base |
to_float |
(s string) -> (float, Error) |
Parse string as floating-point number |
to_bool |
(s string) -> (bool, Error) |
Parse string as boolean |
Behavior:
- When assigned to a single variable (
mut n int = strconv.to_int("42")), panics on invalid input. - When destructured (
mut n, err = strconv.to_int("42")), returns an Error instead of panicking.
to_int / to_uint rules:
- The
baseparameter must be an integer between 2 and 36 (inclusive). Invalid bases produce compile-time error E5009 when passed as a literal, or a runtime panic/error when passed as a variable. - Leading/trailing whitespace is not tolerated — the entire string must be a valid representation.
to_uintrejects strings containing a-sign (returns error or panics).- For bases > 10, letters
A–Z(case-insensitive) represent digits 10–35.
to_float rules:
- Accepts standard decimal notation (e.g.
"3.14","-0.5","100"). - Does not accept hex floats, infinity, or NaN strings.
to_bool rules:
- Accepts
"true"and"false"(case-insensitive:"TRUE","True","FALSE", etc. are valid). - All other strings produce an error or panic. There is no implicit truthiness —
"1","0","yes","no"are not valid.
| Function | Signature | Description |
|---|---|---|
from_int |
(n int) -> string |
Convert integer to decimal string |
from_uint |
(n uint) -> string |
Convert unsigned integer to decimal string |
from_float |
(f float) -> string |
Convert float to string (shortest representation) |
from_bool |
(b bool) -> string |
Convert boolean to "true" or "false" |
These functions never fail.
| Function | Signature | Description |
|---|---|---|
is_numeric |
(s string) -> bool |
Returns true if string is a valid numeric representation (integer or decimal) |
is_integer |
(s string) -> bool |
Returns true if string is a valid integer (digits only, optional leading sign) |
is_numeric rules:
- Accepts optional leading
+or-, followed by digits with at most one.decimal point. - Empty strings return
false. - Does not accept scientific notation, hex prefixes, or whitespace.
is_integer rules:
- Accepts optional leading
+or-, followed by one or more digits (0–9). - Empty strings return
false. - Does not validate whether the value fits in an
intoruint.
| Constant | Value | Description |
|---|---|---|
BASE_2 |
2 |
Binary |
BASE_8 |
8 |
Octal |
BASE_10 |
10 |
Decimal (default) |
BASE_16 |
16 |
Hexadecimal |
BASE_36 |
36 |
Base-36 (digits + full alphabet) |
Constants can be used qualified (strconv.BASE_16) or bare after import and use @strconv. Any integer value between 2 and 36 is also accepted directly as the base argument.
The Error type represents an error condition. Errors are created with the error() function:
mut err Error = error("something went wrong")
Functions that may fail conventionally return a tuple with the result and an Error:
do read_file(path string) -> (string, Error) {
if !file_exists(path) {
return "", error("file not found")
}
return contents, nil
}
Errors are checked by comparing to nil:
mut content, err = read_file("data.txt")
if err != nil {
println("Error: ${err}")
return
}
// Use content
Certain operations produce runtime errors that terminate program execution:
- Division by zero (int or float)
- Array index out of bounds
- Map key not found
- Invalid type conversion
Runtime errors include location information (file, line, column).
EZ uses scope-based automatic memory management. When a block of code ends — a function body, a loop iteration, or a conditional block — any memory it created is freed. If a value needs to survive because it escapes the scope, EZ handles it automatically.
do process(name string) {
mut upper = strings.to_upper(name) // allocated in this scope
mut parts = strings.split(upper, ",") // allocated in this scope
println(parts[0])
}
// function ends -> upper and parts are freed
No imports, no annotations, no cleanup calls.
When a value is stored somewhere that outlives the current scope, EZ copies it to the outer scope:
mut results [string] = {}
for_each line in lines {
mut upper = strings.to_upper(line)
arrays.append(results, upper) // upper escapes into results
}
// results lives until its scope ends
// each iteration's other temporaries are freed
Three cases where EZ keeps values alive:
- Returning a value — the return value is copied to the caller's scope
- Storing into an outer-scope container — array append, map insert, struct field assignment
- Assigning to a variable declared in an outer scope
Everything else is freed when the block ends.
Primitive types (int, uint, float, bool, char, byte) are stack-allocated. Compound types (string, arrays, maps, structs created with new()) are allocated in the current scope's memory region and freed when that scope ends.
Primitive types (int, uint, float, string, bool, char, byte) have value semantics. Assignment creates a copy:
mut a int = 42
mut b int = a // b is a copy of a
b = 100 // a is still 42
Composite types (arrays, maps) have reference semantics for assignment but value semantics for function parameters (unless the parameter is declared mutable).
The copy() function creates a deep copy of any value, including nested structures:
mut original = Person{name: "Alice", age: 30}
mut duplicate = copy(original)
duplicate.age = 31 // original.age is still 30
The new() function allocates a zero-initialized struct in the current scope and returns a pointer to it:
| Type | Zero Value |
|---|---|
int/uint |
0 |
float |
0.0 |
string |
"" |
bool |
false |
char |
'\0' |
byte |
0 |
map[K:V] |
{} |
| struct | All fields zero-initialized |
Three block types create memory scopes:
- Function bodies — temporaries freed on return, return values survive by copying to the caller's scope
- Loop iterations (
for,for_each,as_long_as) — each iteration's temporaries freed, values that escape into outer-scope containers survive - Conditional blocks (
if,or,otherwise) — temporaries freed on block exit, values assigned to outer-scope variables survive
Nested scopes work correctly — a loop inside an if inside a function creates three scope levels, each cleaning up independently.
The @mem module provides explicit arena control for power users who need it:
import @mem
mut scratch = mem.arena(4096)
mut node = mem.init(scratch, Node)
// ... use node ...
mem.reset(scratch)
mem.destroy(scratch)
Most users never import the @mem module. The automatic scope model handles their allocations.
EZ compiles to C and is not memory safe in the way that Rust or similar languages are. However, the scope-based memory model prevents many common memory errors automatically, and the compiler catches several more at compile time.
Compile-time checked:
| Hazard | EZ Behavior |
|---|---|
| Returning address of local variable | E3063 — addr() of a local cannot appear in a return statement |
| Cross-scope pointer assignment | W3004 — warning when a pointer in an outer scope is assigned from addr() of a value in an inner scope |
Double-free on @mem arenas |
E3064 — straight-line double mem.destroy() on the same variable is rejected |
Prevented by the scope model:
| Hazard | How |
|---|---|
| Memory leaks in long-running programs | Scopes free allocations on exit |
| Use-after-free (common case) | Out-of-scope values can't be named — if you can't reach it, it's freed |
| Dangling returns (common case) | Return values are copied to the caller's scope — the data moves, the pointer stays valid |
| Loop memory accumulation | Each iteration is a scope; temporaries cleaned up on iteration end |
Runtime-checked (safe by default):
| Hazard | EZ Behavior |
|---|---|
| Nil pointer dereference | Runtime panic |
| Array out-of-bounds | Runtime panic |
| Map key not found | Runtime panic |
| Division by zero | Runtime panic |
| Integer overflow | Runtime panic (checked arithmetic) |
| Stack overflow (deep recursion) | Detected and reported |
Double-free on @mem arenas (conditional/cross-function) |
Runtime panic |
Not checked (programmer responsibility):
| Hazard | When It Can Happen |
|---|---|
Use-after-free (@mem only) |
Holding a pointer to @mem arena memory after mem.destroy() |
| Data races | Multiple threads accessing shared data without sync.lock() |
| Pointer arithmetic | Not supported in the language (disallowed by design) |
For most EZ programs — those that don't use the @mem module, raw pointers, or threading — the combination of scope-based cleanup, compile-time checks, and runtime panics provides practical safety without annotations or manual memory management.
An EZ program consists of one or more source files that are compiled to a native binary by the EZ compiler. Each file may contain:
- Import declarations
- Using declarations
- Top-level declarations (functions, structs, enums, constants)
Every EZ program must define a main function with no parameters and no return value:
do main() {
// Program starts here
}
The main function is not called explicitly; it is invoked automatically when the program runs.
main() exits when control reaches its closing brace. An explicit return statement inside main() is not allowed (E3073) — to terminate early, branch the remaining work behind an if/otherwise, or call exit(code) to end the program with a specific status.
Expressions are evaluated left-to-right. Function arguments are evaluated before the function is called.
Short-circuit evaluation applies to && and ||:
// If a is false, b() is not called
if a && b() { ... }
// If a is true, b() is not called
if a || b() { ... }
A program terminates when:
- The
mainfunction returns - A runtime error occurs
The ez command-line tool provides the following commands:
| Command | Description |
|---|---|
ez <file.ez> |
Compile and run a source file |
ez build <file.ez> -o <name> |
Compile to a distributable binary |
ez check <file.ez> |
Type-check without compiling |
ez repl |
Start interactive REPL mode |
ez watch <file.ez> |
Watch for changes and re-run on save |
ez doc <path> |
Generate documentation from #doc attributes |
ez pz <name> |
Scaffold a new project |
ez test |
Run the full test suite |
ez report |
Print system info for bug reports |
ez update |
Check for updates and upgrade to the latest stable |
ez update --pre |
Upgrade to the latest pre-release (alpha/beta/rc) |
ez install <version> |
Install a specific version by exact semver |
ez version |
Show version information |
Prints system information for filing bug reports:
EZ Bug Report Info
==================
EZ Version: v3.0.0
Commit: abc1234
OS: darwin/arm64
RAM: 16 GB
Runs all test suites in sequence:
- Compiler unit tests
- Compiler end-to-end tests
- Compiler integration tests
- CLI integration tests
Exits with status 1 if any suite fails.
This document is the authoritative specification for the EZ programming language.