ββββββββββββββββ βββ ββββββ ββββ βββ βββββββ
ββββββββββββββββ βββ βββββββββββββ βββββββββββ
ββββββ βββββ βββ ββββββββββββββ ββββββ ββββ
ββββββ βββββ βββ βββββββββββββββββββββ βββ
ββββββββββββββββ βββββββββββ ββββββ βββββββββββββββ
ββββββββββββββββ βββββββββββ ββββββ βββββ βββββββ
A dynamically-typed, bytecode-compiled programming language with natural English syntax and an optional static type checker.
Quick Start Β· Syntax Guide Β· Type Checker Β· Built-ins Β· OOP Β· Async Β· Native FFI Β· GUI Β· Bundling
This document is written directly from the C++ source (src/) of this repository β the lexer, parser, type checker, bytecode compiler, VM, and the runtime builtins that are actually registered in C++. Anything implemented purely as an EZ-language standard-library package (in the separate ezlib registry β math, crypto, db, pdf, fs, os, gui, collections, etc.) is not part of this repository and is called out explicitly where relevant, since those packages aren't present here and their exact API can't be verified from this codebase.
EZ is a scripting language built from scratch β bytecode compiler, stack VM, garbage collector, optional static type checker, and a Win32 GUI/FFI layer β all written in C++17. It replaces conventional keywords with plain English: when instead of if, task instead of function, give instead of return, other instead of else.
# A taste of EZ
task greet(name) {
give "Hello, " + name + "!"
}
out greet("World") # Hello, World!
people = ["Alice", "Bob", "Carol"]
get person in people {
out greet(person)
}
- Architecture
- Installation & Building
- Quick Start
- Language Syntax
- Optional Static Type Checker
- Object-Oriented Programming
- Error Handling
- Modules (
use) - Built-in Functions (C++ Runtime)
- Async & Concurrency
- Native FFI
- GUI Framework (raw builtins)
- Bundling Standalone Executables
- REPL
- The
ezlibStandard Library (external) - Known Gaps & Caveats
- Contributing
source.ez
β
βΌ Lexer.cpp (581 lines, ~50 token types)
Token stream
β
βΌ Parser.cpp (1,411 lines, recursive descent)
AST (std::variant β 17 expression + ~20 statement node types, in AST.h)
β
βΌ TypeChecker.cpp (673 lines) β OPTIONAL static analysis pass
Β· Validates type annotations on vars, params, returns, structs
Β· Catches: type mismatches, 'self' misuse, break/continue outside loops
β
βΌ BytecodeCompiler.cpp (1,774 lines)
Β· Upvalue resolution Β· Constant folding
Β· TCO detection Β· Scope management Β· static-local desugaring
Bytecode chunks (~85 opcodes, stack-VM ISA β see Bytecode.h)
β
βΌ BytecodeVM.cpp (2,286 lines)
Β· Closure capture Β· Async/Future dispatch (via EventLoop)
Β· Exception handling Β· Interface validation
Β· Operator-overload dispatch on model instances
Runtime values (Value.h β std::variant, 19 alternatives, O(1) type lookup)
β
βΌ GarbageCollector (GC.cpp/.h)
Β· Cycle-detecting mark-sweep over an intrusive doubly-linked list
Β· Threshold-triggered (default 50,000 allocations)
Β· Stop-the-world coordination across spawned OS threads
Source layout (src/):
| File | Lines | Role |
|---|---|---|
BytecodeVM.cpp / .h |
2,286 / 160 | The interpreter loop, opcode dispatch, operator overloading, FFI crash guards |
BytecodeCompiler.cpp / .h |
1,774 / 173 | AST β bytecode, scope/upvalue resolution, constant folding |
Parser.cpp / .h |
1,411 / 95 | Recursive-descent parser β AST |
GUIBuiltins.cpp / .h |
1,214 / β | ~70 raw Win32/GDI+ gui_* native functions |
runtime/Builtins_Sys.cpp |
766 | FFI (os_*), console I/O, concurrency primitives, exceptions |
AST.h |
715 | All expression & statement node definitions |
TypeChecker.cpp / .h |
673 / 128 | Optional static type-checking pass |
main.cpp |
640 | CLI entry point, REPL, bundle packer, PE patching |
Lexer.cpp / .h |
581 / β | Tokenizer, string interpolation desugaring |
Value.h |
558 | The 19-variant runtime Value type |
runtime/Builtins_Data.cpp |
449 | Array/dict/JSON/metaprogramming builtins |
Bytecode.cpp / .h |
333 / 311 | Opcode definitions, constant pool, disassembler |
runtime/Builtins_Net.cpp |
236 | http_get/http_post/fetch/URL encoding |
Token.h |
224 | Token type enum and keyword table |
runtime/Builtins_String.cpp |
216 | String manipulation builtins |
GC.cpp / .h |
187 / 178 | Garbage collector |
PackageManager.h |
284 | ez install / ez init / ez list |
BytecodeCompiler.h |
173 | Compiler scope/local/upvalue bookkeeping |
runtime/Builtins_Concurrency.cpp |
124 | spawn, Mutex, Atomic, wait/waitAsync |
runtime/Builtins_IO.cpp |
113 | File read/write/append/lines |
EZFuture.h |
120 | Future/promise object for async/await |
runtime/Builtins_Buffer.cpp |
104 | buffer(), buf_* raw byte buffers |
runtime/Builtins_Math.cpp |
104 | floor/ceil/abs/sqrt/pow/rand/... |
MiniJson.h |
179 | JSON parser/serializer used by parse_json/to_json |
Total: ~14,700 lines of C++ across the files actually present in src/, targeting Windows x64 with C++17 (the code includes <windows.h> directly and uses MinGW/MSVC-specific structures, so it is Windows-only).
| Dependency | Purpose |
|---|---|
| C++17 compiler (MinGW-w64 / MSVC) | Building the interpreter |
| CMake 3.10+ | Build system |
| libcurl | HTTP client (http_get, http_post, fetch) |
| libsqlite3 | Linked by CMake, though no db_* builtins currently exist in C++ (see Known Gaps) |
Win32 SDK (dwmapi, uxtheme) |
GUI dark-mode/theme APIs |
git clone https://github.com/imabd645/EZ-language.git
cd EZ-language
mkdir build && cd build
cmake .. -G "MinGW Makefiles"
cmake --build .This produces ez.exe and links against sqlite3 curl ws2_32 pthread (plus dwmapi uxtheme on Windows), per CMakeLists.txt.
Note:
CMakeLists.txt'sSOURCESlist currently does not includeruntime/Builtins_Buffer.cpporruntime/Builtins_Concurrency.cpp, and references aruntime/Builtins_DB.cppthat does not exist in the tree. A manualg++invocation listing every file undersrc/andsrc/runtime/(as below) is the reliable way to get a complete build with buffer/concurrency/FFI support.
g++ -std=c++17 -O2 -o ez.exe \
src/main.cpp src/Lexer.cpp src/Parser.cpp src/TypeChecker.cpp \
src/Bytecode.cpp src/BytecodeCompiler.cpp src/BytecodeVM.cpp \
src/Builtins.cpp src/GUIBuiltins.cpp \
src/GC.cpp src/GCObject.cpp \
src/runtime/Builtins_IO.cpp src/runtime/Builtins_Math.cpp \
src/runtime/Builtins_Net.cpp src/runtime/Builtins_String.cpp \
src/runtime/Builtins_Data.cpp src/runtime/Builtins_Sys.cpp \
src/runtime/Builtins_Buffer.cpp src/runtime/Builtins_Concurrency.cpp \
src/runtime/EventLoop.cpp \
-lsqlite3 -lcurl -lws2_32 -lpthread -ldwmapi -luxtheme \
-I src- Move
ez.exeto a permanent folder (e.g.C:\ez\) - Open System Properties β Environment Variables β Path β Edit β New
- Add
C:\ez - Restart your terminal
ez --help # show usage
ez hello.ez # run a script
ez --trace hello.ez # run with bytecode execution tracing
ez # start the REPLout "Hello, World!"
ez hello.ez
# Hello, World!# Variables β no declaration keyword needed
name = "Abdullah"
age = 19
pi = 3.14159
active = true
out "Name: " + name
out "Age: " + str(age)
# Conditional
when age >= 18 {
out name + " is an adult"
} other {
out name + " is a minor"
}
# repeat i = N to M β inclusive on both ends, auto-detects reverse direction
repeat i = 1 to 5 {
out "Count: " + str(i)
}
# For-each loop
fruits = ["apple", "banana", "cherry"]
get fruit in fruits {
out fruit
}
# Function
task square(n) {
give n * n
}
out str(square(7)) # 49
# Lambda
double = |x| x * 2
out str(double(5)) # 10
EZ supports three comment styles:
# Hash comment (line)
// Double-slash comment (line)
/* Block comment, supports /* nesting */ */
Variables are dynamically typed by default (optional static types are covered in the Type Checker section below). No declaration keyword is needed β just assign.
# Numbers β two internal runtime types: INTEGER (long long) and NUMBER (double)
x = 42 # INTEGER
pi = 3.14159 # NUMBER (double)
big = 0xFF # hex literal -> INTEGER (255)
# Strings (single or double quotes β both behave the same)
msg = "Hello, EZ!"
msg2 = 'also fine'
# Raw strings β prefix r before a quote disables escape processing
raw = r"C:\Users\file.txt"
# Booleans β yes/no are aliases for true/false
flag1 = true
flag2 = yes
flag3 = false
flag4 = no
# Nil
nothing = nil
# Compound assignment
x += 5 # x = x + 5
x -= 2
x *= 3
x /= 4
Runtime type names (returned by typeOf()): "nil", "bool", "integer", "float", "string", "array", "dictionary", "function", "model", "instance", "future", "buffer", "mutex", "interface", "super".
Note:
typeOfdistinguishes"integer"(whole numbers, stored aslong longfor fast arithmetic) from"float"(doubles). Both satisfyisNumber()checks in the runtime.
EZ has three string forms:
"double quoted" # supports escapes: \n \t \r \0 \\ \" \' and \xNN (hex byte)
'single quoted' # identical semantics to double quotes
r"raw string" # backslashes are literal β no escape processing
Template strings use backticks with {expr} interpolation (not ${...}):
name = "Abdullah"
age = 19
out `Hello {name}, you are {age + 1} next year`
# -> Hello Abdullah, you are 20 next year
Internally the lexer desugars a backtick template into string concatenation with str(...) calls around each {...} expression β so `A{1+1}B` becomes ("A" + str(1+1) + "B"). A backtick string with no {} interpolation is just a plain string literal.
# Arithmetic
out 10 + 3 # 13
out 10 - 3 # 7
out 10 * 3 # 30
out 10 / 3 # 3.333...
out 10 % 3 # 1 (modulo)
# Comparison
out 5 == 5 # true
out 5 != 4 # true
out 3 < 4 # true
out 4 <= 4 # true
out 5 > 3 # true
out 5 >= 5 # true
# Logical
out true and false # false
out true or false # true
out not true # false
# Bitwise
out 10 & 12 # 8 (AND)
out 10 | 5 # 15 (OR)
out 10 ^ 12 # 6 (XOR)
out ~5 # bitwise NOT
out 1 << 3 # 8 (left shift)
out 16 >> 2 # 4 (right shift)
# Ternary
label = age >= 18 ? "adult" : "minor"
# Spread (in array literals and function calls)
a = [1, 2, 3]
b = [...a, 4, 5] # [1, 2, 3, 4, 5]
**is not a real operator. Although aPOWopcode exists in the bytecode ISA, the lexer never produces a**/power token β there is only a single*(multiply) operator. For exponentiation, use thepow(base, exp)builtin. Writing2 ** 8will lex as two consecutive*tokens and will not parse as exponentiation.
0b...binary literals are not supported by the lexer β only decimal numbers and0x...hexadecimal literals are recognized.
score = 85
when score >= 90 {
out "A"
} other when score >= 80 {
out "B"
} other when score >= 70 {
out "C"
} other {
out "F"
}
other when ... is parsed as a single nested when statement attached as the else branch β this is the only way to chain conditions. A bare when block immediately followed by another when block (without other) is parsed as two independent statements, not an if/else-if chain.
EZ also supports a brace-less single-statement form for short conditionals:
when x == 5 { escape }
when x % 2 == 0 { skip }
n = 1
while n <= 10 {
out str(n)
n += 1
}
The compiler emits loopVar <= endVar as the loop condition β both bounds are inclusive. If N > M (and both are literal constants), the compiler automatically detects this and emits a decrementing loop (loopVar >= endVar) instead.
repeat i = 1 to 5 {
out str(i) # prints 1 2 3 4 5
}
repeat i = 5 to 1 {
out str(i) # prints 5 4 3 2 1 (auto reverse)
}
# Iterate an array
get item in ["alpha", "beta", "gamma"] {
out item
}
# Iterate dictionary keys
config = {"host": "localhost", "port": 8080}
get key in config {
out key + " = " + str(config[key])
}
# Destructure key AND value
get [k, v] in config {
out k + " = " + str(v)
}
state = "success"
match state {
"loading" => out "Please wait..."
"success" => {
out "Operation completed!"
}
other => out "Unknown state"
}
Each arm is <expression> => <statement-or-block>; the special other arm (no expression) is the default/fallback case. There is no support for value ranges, multiple values per arm, or destructuring patterns in match β each arm pattern is evaluated and compared with == against the subject.
escapeβ break out of the nearest enclosing loop (while,repeat,get).skipβ continue to the next iteration.
repeat i = 1 to 10 {
when i == 5 { escape }
when i % 2 == 0 { skip }
out str(i)
}
# Basic function
task add(a, b) {
give a + b
}
out str(add(3, 4)) # 7
# Default parameters
task greet(name, greeting) {
when not greeting { greeting = "Hello" }
give greeting + ", " + name + "!"
}
# Variadic functions (... prefix on the last parameter)
task sum(...nums) {
total = 0
get n in nums { total += n }
give total
}
out str(sum(1, 2, 3, 4, 5)) # 15
# Recursive function
task factorial(n) {
when n <= 1 { give 1 }
give n * factorial(n - 1)
}
# Tail-call optimized recursion (give f(...) in tail position reuses
# the current stack frame β supports 20,000+ levels of recursion)
task count(n, acc) {
when n == 0 { give acc }
give count(n - 1, acc + 1) # TCO
}
# Lambda β single expression
double = |x| x * 2
# Lambda β multi-statement body
clamp = |val, lo, hi| {
when val < lo { give lo }
when val > hi { give hi }
give val
}
# Closures β Lua-style upvalues, migrate to the heap when their
# enclosing scope exits
task makeCounter() {
count = 0
give || {
count += 1
give count
}
}
counter = makeCounter()
out str(counter()) # 1
out str(counter()) # 2
out str(counter()) # 3
A static name = expr statement inside a task body declares a variable whose initializer runs only on the first call β its value persists across subsequent calls to the same function, similar to a static local in C:
task tick() {
static count = 0
count += 1
give count
}
out str(tick()) # 1
out str(tick()) # 2
out str(tick()) # 3
Internally each static variable is compiled to a uniquely-mangled global (__static_<compilerId>_<funcName>_<varName>) and is only initialized the first time the declaration executes (checked via a HAS_GLOBAL test).
# Array literals
primes = [2, 3, 5, 7, 11]
# Indexing (0-based, supports negative indices from the end)
out str(primes[0]) # 2
out str(primes[-1]) # 11
# Array operations
push(primes, 13)
pop(primes)
out str(len(primes)) # 5
# Slice
out str(primes[1:3]) # [3, 5]
# Spread into function call
args = [3, 4]
out str(add(...args))
# Dictionary (hash map) β keys are strings
user = {
"name": "Alice",
"age": 30,
"tags": ["admin", "user"]
}
out user["name"] # Alice
out str(user["age"]) # 30
out str(has_key(user, "email")) # false
user["email"] = "alice@example.com"
get key in user {
out key + ": " + str(user[key])
}
out str(keys(user)) # ["name", "age", "tags", "email"]
out str(len(user)) # 4
dictRemove(user, "tags")
EZ ships with an opt-in static type checker (TypeChecker.cpp, ~673 lines) that runs after parsing and before bytecode compilation. If it reports an error, the script exits with status 65 before any code runs. Type annotations are entirely optional β untyped code (everything shown above) checks fine because unannotated variables default to type Any, which is compatible with everything.
# Variable declarations with type annotations
x: bool = true
i: number = 0
arr: Array[number] = [1, 2, 3]
dict: Dict[string, number] = {"a": 1, "b": 2}
# Function/task signatures
task add(a: number, b: number) -> number {
give a + b
}
# Struct fields with types and defaults
struct User {
name: string
age: number = 25
}
# Interface method signatures
interface Logger {
task log(message: string, level: number) -> bool
}
- Type mismatches in variable declarations (
arr[1] = "string"wherearr: Array[number]β error). - Dictionary key/value types:
dict["key"]must match the declaredDict[K, V]key type, e.g.dict[1] = 2on aDict[string, number]is an error. - Logical operator operands:
and/oroperands must bebool(x and "string"β error). selfusage:selfcan only be referenced inside amodel'sinitor methods β usingselfat top-level is an error.- Loop control statements:
escape/skip(break/continue) outside of any loop is an error. - Generic container types:
Array[T]andDict[K, V]with nested type arguments are supported in the type grammar (TypeInfo/TypeAST). - Any type written as
Any(or omitted) is treated as compatible with everything β it's the universal escape hatch for dynamic code.
If you never write : type annotations, this entire pass is effectively a no-op pass-through, and EZ behaves as a purely dynamically-typed language.
model Animal {
init(name, sound) {
self.name = name
self.sound = sound
}
# Public method
shown speak() {
out self.name + " says " + self.sound
}
# Private method
hidden _describe() {
give "I am " + self.name
}
# toString override β called by out and str()
task toString() {
give "Animal(" + self.name + ")"
}
}
cat = Animal("Cat", "meow")
cat.speak() # Cat says meow
out str(cat) # Animal(Cat)
model Dog extends Animal {
init(name) {
super.init(name, "woof")
self.tricks = []
}
task learnTrick(trick) {
push(self.tricks, trick)
give self # enables method chaining
}
shown perform() {
get trick in self.tricks {
out self.name + " performs: " + trick
}
}
}
rex = Dog("Rex")
rex.learnTrick("sit").learnTrick("roll over")
rex.speak() # Rex says woof
rex.perform() # Rex performs: sit
# Rex performs: roll over
When a class extends a parent, all of the parent's methods are copied into the child's method table at class-creation time (unless overridden) β inheritance is implemented as a one-time method-table merge, not a runtime lookup chain.
interface Serializable {
task toJson()
task fromJson(json)
}
model Config implements Serializable {
init(data) {
self.data = data
}
task toJson() {
give to_json(self.data)
}
task fromJson(json) {
self.data = parse_json(json)
give self
}
}
cfg = Config({"debug": true, "port": 3000})
out cfg.toJson()
When a model with implements SomeInterface is created, the VM checks (at class-creation time) that every method named in the interface exists in the class's method table. If a required method is missing, EZ raises a runtime error: Model 'X' fails to implement interface 'Serializable': missing task 'fromJson'. This is presence-only validation β argument and return types of interface methods are not checked at runtime.
model Counter {
static count = 0
init() {
Counter.count += 1
self.id = Counter.count
}
static task reset() {
Counter.count = 0
}
static task total() {
give Counter.count
}
}
a = Counter()
b = Counter()
c = Counter()
out str(Counter.total()) # 3
Counter.reset()
out str(Counter.total()) # 0
struct Point {
x, y
}
p = new Point()
p.x = 10
p.y = 20
out str(p.x) + ", " + str(p.y)
Structs may also carry type annotations and default values when used with the type checker:
struct User {
name: string
age: number = 25
}
The parser allows any token to be used as a function name inside a model body β this is what enables operator overloading. The bytecode VM checks for a matching method on a model instance before falling back to the built-in numeric/structural behavior.
Overloadable (looked up by name on the left-hand instance):
| Operator | Method name | Notes |
|---|---|---|
+ |
task +(other) |
|
- (binary) |
task -(other) |
|
* |
task *(other) |
|
/ |
task /(other) |
|
== |
task ==(other) |
|
< |
task <(other) |
|
> |
task >(other) |
|
>= |
task >=(other) |
|
- (unary negation) |
task neg() |
invoked for -v |
Not overloadable β <= and != always fall back to the default numeric/structural comparison (doLessEq / doNotEqual) regardless of whether the instance defines <= or != methods. If you need consistent <=/!= behaviour for a custom type, define < and == and compose them at the call site, or expose a named helper method like lte().
model Vector {
init(x, y) {
self.x = x
self.y = y
}
task +(other) {
give Vector(self.x + other.x, self.y + other.y)
}
task ==(other) {
give self.x == other.x and self.y == other.y
}
task neg() {
give Vector(-self.x, -self.y)
}
task toString() {
give "Vector(" + str(self.x) + ", " + str(self.y) + ")"
}
}
v1 = Vector(10, 20)
v2 = Vector(5, 5)
v3 = v1 + v2 # Vector(15, 25)
v1 += v2 # desugars to v1 = v1 + v2
out str(v1 == v3) # true
v4 = -v2 # Vector(-5, -5)
task divide(a, b) {
when b == 0 {
throw "Division by zero"
}
give a / b
}
try {
result = divide(10, 0)
} catch e {
out "Caught: " + str(e) # Caught: Division by zero
}
catch (TypeName e) matches only if the thrown value is an instance of TypeName (or one of its subclasses, via extends). Multiple typed catch clauses can be chained, with an untyped catch e as a final catch-all:
model MathError {
init(msg) { self.msg = msg }
}
model NetworkError {
init(msg) { self.msg = msg }
}
try {
throw MathError("division by zero")
} catch (MathError e) {
out "Math error: " + e.msg
} catch (NetworkError e) {
out "Network error: " + e.msg
} catch e {
out "Unknown: " + str(e)
}
Subclasses (model DerivedError extends MathError) are caught by a catch (MathError e) clause as well β the VM walks the instance's class chain when matching catch types.
A native Exception(message, code) constructor returns a plain dictionary with message, code, and stackTrace keys β useful as a lightweight, allocation-free alternative to defining your own error models:
err = Exception("Not found", 404)
out err["message"] # Not found
out str(err["code"]) # 404
try {
try {
throw "inner"
} catch e {
out "Inner handler: " + str(e)
throw "re-thrown"
}
} catch e {
out "Outer handler: " + str(e)
}
panic(message) immediately raises a fatal runtime error (the same mechanism used for internal VM errors) β it is not catchable by try/catch and terminates the script with exit code 70.
# Import entire file β all its globals become available
use "lib/utils.ez"
# Namespaced import β access via alias.name
use "lib/math.ez" as math
out str(math.sqrt(25))
# Import everything from a module into the current scope
use "lib/helpers.ez" as *
# Import an installed package (after `ez install <name>`)
use "collections"
use resolves paths relative to lib/<name>.ez, lib/<name>/main.ez, C:/ezlib/<name>.ez, or C:/ezlib/<name>/main.ez, in addition to the literal path given. This same resolution logic is also how ez bundle discovers transitive dependencies to pack into a standalone executable.
Everything in this section is registered directly as a native function in the C++ source (src/Builtins.cpp and src/runtime/Builtins_*.cpp) and is always available, with no use statement required.
| Function | Description |
|---|---|
out expr |
Print value with newline (language statement, not a function call) |
print(val) |
Print without a trailing newline |
input(prompt) / __input__ |
Read a line from stdin |
color(code) |
Set Windows console text color |
reset() |
Reset console color |
clear() |
Clear the console screen |
gotoxy(x, y) |
Move console cursor |
getch() |
Read a single character without echo |
| Function | Description |
|---|---|
str(val) |
Convert any value to string |
num(val) |
Parse string/value to number |
typeOf(val) / type(val) |
Return the runtime type name as a string |
len(val) |
Length of string, array, or dictionary |
| Function | Description |
|---|---|
substr(s, start, len) / substring(s, start, end) |
Extract substring |
split(s, delim) |
Split string to array |
join(arr, delim) |
Join array to string |
upper(s) / toUpper(s) |
Uppercase |
lower(s) / toLower(s) |
Lowercase |
trim(s) |
Strip leading/trailing whitespace |
replace(s, old, new) |
Replace substring |
contains(s, sub) |
Substring check (also works on arrays for membership) |
startsWith(s, prefix) |
Prefix check |
endsWith(s, suffix) |
Suffix check |
indexOf(s_or_arr, val) |
First index, or -1 |
ord(char) |
Character β ASCII code |
chr(code) |
ASCII code β character |
| Function | Description |
|---|---|
push(arr, val) |
Append to array |
pop(arr) |
Remove and return last element |
insert(arr, idx, val) |
Insert at index |
remove(arr, val_or_idx) |
Remove an element |
reverse(arr) |
Reverse in place |
sort(arr) |
Sort in place |
slice(arr, start, end) |
Return sub-array |
range(start, end[, step]) |
Generate an array of numbers |
filter(arr, fn) |
Return elements where fn returns true |
map(arr, fn) |
Transform each element |
reduce(arr, fn, init) |
Fold array to a single value |
forEach(arr, fn) |
Call fn for each element (no return value) |
every(arr, fn) |
True if fn is true for all elements |
some(arr, fn) |
True if fn is true for any element |
find(arr, fn) |
First element matching fn, or nil |
keys(dict) |
Array of dictionary keys |
values(dict) |
Array of dictionary values |
has_key(dict, key) |
Boolean key check |
dictRemove(dict, key) |
Delete a key |
| Function | Description |
|---|---|
parse_json(str) |
Parse a JSON string to an EZ value (via MiniJson.h) |
to_json(val) |
Serialize an EZ value to a JSON string |
| Function | Description |
|---|---|
readFile(path) |
Read entire file as a string |
writeFile(path, content) |
Write/overwrite a file |
appendFile(path, content) |
Append to a file |
readLines(path) |
Read file as an array of lines |
writeLine(path, line) |
Write a single line |
appendLine(path, line) |
Append a single line |
| Function | Description |
|---|---|
http_get(url[, headers]) |
HTTP GET |
http_post(url, body[, headers]) |
HTTP POST |
fetch(url[, options]) |
Generic HTTP request helper |
url_encode(str) / url_decode(str) |
URL component encoding |
http_put,http_delete, andstartServer(port, handler)(web server) are not present as C++ builtins in this repository.
| Function | Description |
|---|---|
reMatch(text, pattern) |
Test if text matches pattern |
reSearch(text, pattern) |
Find first match, return captured groups |
reReplace(text, pattern, repl) |
Replace first match |
| Function | Description |
|---|---|
floor(x) |
Round down |
ceil(x) |
Round up |
round(x) |
Round to nearest integer |
abs(x) |
Absolute value |
sqrt(x) |
Square root |
pow(base, exp) |
Exponentiation (use this instead of **) |
min(a, b) |
Minimum of two values |
max(a, b) |
Maximum of two values |
rand() |
Random float in [0, 1) |
randint(lo, hi) |
Random integer in range |
| Function | Description |
|---|---|
buffer(size_or_string) |
Allocate a raw byte buffer, or build one from a string |
buf_size(buf) |
Buffer length in bytes |
buf_fill(buf, byteVal) |
Fill the buffer with a byte value |
buf_copy(src, dst, ...) |
Bulk-copy bytes between buffers |
buf_to_str(buf) |
Decode buffer contents as a UTF-8 string |
buf[i] / buf[i] = v |
Index a buffer directly to read/write a byte |
| Function | Description |
|---|---|
spawn(fn, ...args) |
Start a detached OS thread running fn(...args) |
await expr / sync expr |
Block until a Future resolves (sync is an alias for await as a function call) |
waitAsync(ms) |
Non-blocking delay (yields to the event loop) |
wait(ms) / stop(ms) |
Blocking sleep for ms milliseconds |
mutex() |
Create a Mutex value |
lock(mu, fn) |
Acquire mutex, run fn, release |
Atomic(initial) |
Create an atomic integer value |
| Function | Description |
|---|---|
getattr(obj, name) |
Get property by name string |
setattr(obj, name, val) |
Set property by name string |
hasattr(obj, name) |
Check if property exists |
| Function | Description |
|---|---|
Exception(message, code) |
Construct a {message, code, stackTrace} dictionary |
panic(message) |
Raise a fatal, uncatchable runtime error |
exit(code) |
Terminate the process immediately |
clock() |
Milliseconds since the Unix epoch (wall-clock, not process start) |
EZ's concurrency model combines a custom native event loop for cooperative async/await with real OS threads (spawn) for CPU-bound parallel work, plus thread-safe primitives (Mutex, Atomic).
async task fetchUser(id) {
data = http_get("https://api.example.com/users/" + str(id))
give data
}
f1 = fetchUser(1)
f2 = fetchUser(2)
out "Fetching in parallel..."
r1 = await f1
r2 = await f2
result = await async {
wait(500)
give "done"
}
# Inline await
out await fetchUser(3)
model SafeCounter {
init() {
self.mu = mutex()
self.value = 0
}
task increment() {
lock(self.mu, || {
self.value += 1
})
}
task get() {
give self.value
}
}
counter = SafeCounter()
futures = []
repeat i = 1 to 10 {
f = spawn(|| { counter.increment() })
push(futures, f)
}
get f in futures { await f }
out str(counter.get()) # 10
When spawn(fn, ...args) launches a new OS thread, the VM exports a snapshot of thread state (exportThreadState) and closes any upvalues captured by fn so the worker thread doesn't hold dangling pointers into the parent VM's stack. Shared mutable state (arrays, dictionaries, model instances) is shared_ptr-based and remains shared across threads β this is why Mutex and Atomic exist, to coordinate access to that shared state safely. The garbage collector coordinates a stop-the-world pause across all active VM threads (active_vm_threads) before each collection.
EZ can call into arbitrary Windows DLL exports through a family of os_* builtins. This is a low-level interface β there is no automatic argument marshalling beyond numbers, strings, booleans, and buffers, and calls are wrapped in a SEH/vectored-exception guard so a bad call raises an EZ runtime error instead of crashing the interpreter.
kernel = os_load_lib("kernel32.dll")
sleepFn = os_get_func(kernel, "Sleep")
returnType is one of "int", "float", "ptr", or "string". Up to 12 arguments are supported; each argument is coerced based on its EZ type β numbers become intptr_t, strings become const char* (via .c_str()), buffers become pointers to their backing storage, and booleans become 0/1.
kernel = os_load_lib("kernel32.dll")
sleepFn = os_get_func(kernel, "Sleep")
os_call(sleepFn, "int", 1000) # Sleep(1000) β pause for 1 second
user32 = os_load_lib("user32.dll")
msgBox = os_get_func(user32, "MessageBoxA")
os_call(msgBox, "int", 0, "Hello from EZ!", "EZ FFI Demo", 0)
If funcPtr is null (library/function not found), or if the call itself crashes (access violation), os_call raises a catchable EZ runtime error rather than terminating the process.
For working with structs and pointers returned by Win32 APIs, EZ exposes direct memory read/write builtins:
| Function | Description |
|---|---|
os_alloc(size) / os_free(ptr) |
Allocate/free a raw memory block, returns its address as an integer |
os_read_byte(ptr, offset) / os_write_byte(ptr, offset, val) |
Single byte |
os_read_uint16/32/64(ptr, offset) / os_write_uint16/32/64(ptr, offset, val) |
Unsigned integers of various widths |
os_read_float32/64(ptr, offset) / os_write_float32/64(ptr, offset, val) |
Floats/doubles |
os_read_string_ptr(ptr) |
Read a null-terminated C string from a pointer |
os_write_string(ptr, offset, str) |
Write a string into memory |
os_buffer_from_ptr(ptr, size) |
Wrap a raw pointer as an EZ buffer |
os_buffer_addr(buf) |
Get the backing address of an EZ buffer |
| Function | Description |
|---|---|
os_struct_alloc(fieldTypeNames) |
Compute and allocate a buffer sized/aligned for a struct described by an array of type-name strings |
os_struct_pack(layout, values[, buffer]) |
Pack EZ values into a buffer according to a layout description |
os_struct_unpack(layout, bufferOrPtr) |
Unpack a buffer/pointer into an array of EZ values according to a layout |
| Function | Description |
|---|---|
os_get_proxy_wndproc() |
Returns a function pointer to an internal window-procedure trampoline (used by the GUI subsystem to route Win32 messages back into EZ callbacks) |
This is the FFI layer used internally by the GUI subsystem to talk to user32.dll/gdi32.dll, and is general enough to call most *32.dll Win32 APIs that take simple scalar/pointer arguments.
The C++ runtime exposes roughly 70 raw gui_* native functions (src/GUIBuiltins.cpp, ~1,200 lines) that wrap a Win32/GDI+ window, control, and drawing layer. These are the actual primitives available in this repository; a higher-level fluent OOP API (gui.window(...).panel(...).button(...), as seen in example scripts via use "lib/gui.ez" or use "ezgame") is an EZ-language wrapper that lives in the external ezlib/example scripts, not in this C++ source.
EZ can package a script and all its used dependencies into a single self-contained .exe:
ez bundle <entry_script.ez> [output.exe] [--gui] [--icon app.ico]How it works:
- The entry script is read and lexed; every
use "path"statement is resolved (againstlib/,C:/ezlib/, and literal paths) and the dependency graph is crawled recursively. - All discovered
.ezfiles (plus the entry script as__main__.ez) are packed into an in-memory virtual file system (VFS) blob:[fileCount][nameLen][name][contentLen][content].... - The current
ez.exebinary is copied to the output path. - If
--icon app.icois given, the icon is injected into the.exe's resources viaBeginUpdateResourceA/UpdateResourceA/EndUpdateResourceA(parsing the.icodirectory and rewriting it as aGRPICONDIRresource). - The VFS blob is appended to the end of the
.exe, followed by a 4-byte size and the 6-byte magic markerEZPKV1. - If
--guiis given, the PE header'sSubsystemfield is patched from console (3) to GUI (2) at byte offsete_lfanew + 92, hiding the console window when the bundled.exeruns.
At startup, ez.exe checks its own file for the trailing EZPKV1 marker; if present, it loads the embedded VFS, and if a __main__.ez entry exists, runs it directly instead of starting the REPL or reading argv[1] as a script path.
Running ez with no arguments starts an interactive REPL:
EZ Language Interpreter v1.0 (Bytecode Mode)
Type 'exit' to quit
>>> x = 5
>>> out x * 2
10
>>> exit
Goodbye!
The REPL supports multi-line input β it counts {/} and keeps prompting with ... until braces balance, then runs the type checker and bytecode compiler/VM on the accumulated input, sharing global state across REPL evaluations within the same session.
CLI summary (ez --help):
| Command | Description |
|---|---|
ez |
Run REPL (interactive mode) |
ez <file.ez> [--trace] |
Run a script file, optionally tracing bytecode execution |
ez install <pkg> [version] |
Install a package from the ezlib registry |
ez list |
List installed packages |
ez init <name> |
Scaffold a new package (main.ez + package.ez) |
ez bundle <file.ez> [out.exe] [--gui] [--icon app.ico] |
Create a standalone executable |
ez --help / -h |
Show usage |
The ezlib repository is a separate registry of EZ-language packages installed with ez install <name> to C:\ezlib\. Packages such as math, crypto, db/orm, pdf, fs, os, datetime, regex, collections, test, log, thread, gui, serve, http, and ai β along with higher-level fluent wrappers like the gui.window()... chained API and the game/ezgame module used in the Snake/Flappy Bird/Fighter Jets demos β are implemented in EZ itself on top of the C++ runtime primitives described above (mainly the os_* FFI and gui_* raw builtins).
Because that repository is not part of this codebase, this README does not document its exact function signatures β refer to ezlib directly for that API surface. What is guaranteed by this repository is the underlying C++ runtime surface: the value types, operators, control flow, OOP system, type checker, the builtins listed in Built-in Functions, the FFI (os_*), and the raw GUI primitives (gui_*) that any ezlib package is ultimately built on.
A summary of places where this repository's source diverges from commonly-circulated descriptions of EZ:
- No
**power operator β the lexer does not produce a power token; usepow(base, exp). - No
0b...binary literals β only decimal and0x...hex literals are lexed. - String interpolation uses
`text {expr}`, not`text ${expr}`. <=and!=cannot be operator-overloaded on model instances, unlike+ - * / == < > >= neg.CMakeLists.txt's source list is incomplete β it omitsBuiltins_Buffer.cppandBuiltins_Concurrency.cpp, and references a non-existentBuiltins_DB.cpp. Use the manualg++command above for a complete build.- No
db_*(SQLite) orpdf_*builtins exist in C++ despitesqlite3being linked by CMake β any database or PDF functionality must come from anezlibpackage. - No
md5,sha256,base64_encode/decode, orhmac_sha256builtins exist in C++ β cryptographic helpers, if available, areezlib-side. - No
deleteFile,listDir,fs_exists,getenv,setenv, orexecbuiltins exist in C++ β onlyreadFile/writeFile/appendFile/readLines/writeLine/appendLine. - No
http_put,http_delete, orstartServerC++ builtins β onlyhttp_get,http_post, andfetch. - Windows-only β the code directly includes
<windows.h>and Win32-specific structures (PE header patching, icon resources,HMODULE/FARPROCFFI), so it cannot be built on Linux/macOS as-is. matcharms support only equality comparison against literal/expression patterns plus anotherdefault β no ranges, guards, or destructuring.other whenis the only valid else-if chain syntax β consecutive barewhenblocks are independent statements, not an if/else-if/else chain.
- Open an issue on GitHub Issues
- Include a minimal reproducing
.ezscript - State your Windows version and compiler (MinGW / MSVC)
git clone https://github.com/imabd645/EZ-language.git
git checkout -b feature/your-feature
# make changes
git commit -m "feat: describe what you did"
git push origin feature/your-feature
# open a PR- 4 spaces for indentation in C++ source
- Descriptive variable names β no single-letter identifiers except loop counters
- Comment any non-obvious logic
- If adding a new builtin, register it in the appropriate
Builtins_*.cppfile viainterp.defineGlobal(...)and ensure the file is included in bothCMakeLists.txtand any manual build commands
EZ Language is released under the MIT License.
Built from scratch by Abdullah Masood
A complete programming language: lexer β parser β optional type checker β bytecode compiler β stack VM β GC β native FFI/GUI layer