Skip to content

imabd645/EZ-language

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

206 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation


β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—    β–ˆβ–ˆβ•—      β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ•—   β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—
β–ˆβ–ˆβ•”β•β•β•β•β•β•šβ•β•β–ˆβ–ˆβ–ˆβ•”β•    β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ•—  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•
β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—    β–ˆβ–ˆβ–ˆβ•”β•     β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ–ˆβ•—
β–ˆβ–ˆβ•”β•β•β•   β–ˆβ–ˆβ–ˆβ•”β•      β–ˆβ–ˆβ•‘     β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘   β–ˆβ–ˆβ•‘
β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—    β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘  β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•
β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•    β•šβ•β•β•β•β•β•β•β•šβ•β•  β•šβ•β•β•šβ•β•  β•šβ•β•β•β• β•šβ•β•β•β•β•β•

A dynamically-typed, bytecode-compiled programming language with natural English syntax and an optional static type checker.

License: MIT Platform Language

Quick Start Β· Syntax Guide Β· Type Checker Β· Built-ins Β· OOP Β· Async Β· Native FFI Β· GUI Β· Bundling


⚠️ A note on this README

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.


What is EZ?

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)
}

Table of Contents


πŸ— Architecture

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).


πŸš€ Installation & Building

Prerequisites

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

Build with CMake

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's SOURCES list currently does not include runtime/Builtins_Buffer.cpp or runtime/Builtins_Concurrency.cpp, and references a runtime/Builtins_DB.cpp that does not exist in the tree. A manual g++ invocation listing every file under src/ and src/runtime/ (as below) is the reliable way to get a complete build with buffer/concurrency/FFI support.

Build directly with g++ (MinGW)

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

Add to PATH

  1. Move ez.exe to a permanent folder (e.g. C:\ez\)
  2. Open System Properties β†’ Environment Variables β†’ Path β†’ Edit β†’ New
  3. Add C:\ez
  4. 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 REPL

πŸŽ“ Quick Start

Hello World

out "Hello, World!"
ez hello.ez
# Hello, World!

Five Minutes of EZ

# 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

πŸ“š Language Syntax

Comments

EZ supports three comment styles:

# Hash comment (line)
// Double-slash comment (line)
/* Block comment, supports /* nesting */ */

Variables & Types

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: typeOf distinguishes "integer" (whole numbers, stored as long long for fast arithmetic) from "float" (doubles). Both satisfy isNumber() checks in the runtime.

Strings & Interpolation

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.

Operators

# 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 a POW opcode exists in the bytecode ISA, the lexer never produces a **/power token β€” there is only a single * (multiply) operator. For exponentiation, use the pow(base, exp) builtin. Writing 2 ** 8 will lex as two consecutive * tokens and will not parse as exponentiation.

0b... binary literals are not supported by the lexer β€” only decimal numbers and 0x... hexadecimal literals are recognized.

Control Flow

when / other when / other (if / else if / else)

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 }

while loop

n = 1
while n <= 10 {
    out str(n)
    n += 1
}

repeat i = N to M (inclusive range loop)

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)
}

get ... in ... (for-each)

# 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)
}

match (pattern matching)

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 and skip

  • 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)
}

Functions & Lambdas

# 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

static locals (persistent variables across calls)

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).

Arrays & Dictionaries

# 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")

πŸ” Optional Static Type Checker

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.

Type annotation syntax

# 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
}

What the type checker validates

  • Type mismatches in variable declarations (arr[1] = "string" where arr: Array[number] β†’ error).
  • Dictionary key/value types: dict["key"] must match the declared Dict[K, V] key type, e.g. dict[1] = 2 on a Dict[string, number] is an error.
  • Logical operator operands: and/or operands must be bool (x and "string" β†’ error).
  • self usage: self can only be referenced inside a model's init or methods β€” using self at top-level is an error.
  • Loop control statements: escape/skip (break/continue) outside of any loop is an error.
  • Generic container types: Array[T] and Dict[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.


🧱 Object-Oriented Programming

Models (Classes)

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)

Inheritance

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.

Interfaces

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.

Static Members

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

Structs

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
}

Operator Overloading

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)

Error Handling

try / catch / throw

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
}

Typed catches and model-based exceptions

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.

Built-in Exception helper

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

Nested try/catch and re-throw

try {
    try {
        throw "inner"
    } catch e {
        out "Inner handler: " + str(e)
        throw "re-thrown"
    }
} catch e {
    out "Outer handler: " + str(e)
}

panic

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.


Modules (use)

# 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.


πŸ”§ Built-in Functions (C++ Runtime)

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.

Output, Input & Console

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

Type Conversion & Inspection

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

String Functions

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

Array & Dictionary Functions

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

JSON

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

File I/O

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

HTTP Client (via libcurl)

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, and startServer(port, handler) (web server) are not present as C++ builtins in this repository.

Regular Expressions

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

Math Functions

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

Buffers (raw byte storage)

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

Concurrency Primitives

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

Metaprogramming

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

Errors & Process Control

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)

⚑ Async & Concurrency

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 + await

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

async { ... } blocks

result = await async {
    wait(500)
    give "done"
}

# Inline await
out await fetchUser(3)

Thread-safe shared state with Mutex

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

Notes on spawn

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.


πŸ”— Native FFI

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.

Loading a library and resolving a function pointer

kernel = os_load_lib("kernel32.dll")
sleepFn = os_get_func(kernel, "Sleep")

Calling a function: os_call(funcPtr, returnType, ...args)

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.

Raw memory access

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

Struct layout helpers

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

Misc

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.


πŸ–₯ GUI Framework

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.

πŸ“¦ Bundling Standalone Executables

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:

  1. The entry script is read and lexed; every use "path" statement is resolved (against lib/, C:/ezlib/, and literal paths) and the dependency graph is crawled recursively.
  2. All discovered .ez files (plus the entry script as __main__.ez) are packed into an in-memory virtual file system (VFS) blob: [fileCount][nameLen][name][contentLen][content]....
  3. The current ez.exe binary is copied to the output path.
  4. If --icon app.ico is given, the icon is injected into the .exe's resources via BeginUpdateResourceA/UpdateResourceA/EndUpdateResourceA (parsing the .ico directory and rewriting it as a GRPICONDIR resource).
  5. The VFS blob is appended to the end of the .exe, followed by a 4-byte size and the 6-byte magic marker EZPKV1.
  6. If --gui is given, the PE header's Subsystem field is patched from console (3) to GUI (2) at byte offset e_lfanew + 92, hiding the console window when the bundled .exe runs.

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.


πŸ–₯ REPL

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 Standard Library (external)

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.


⚠️ Known Gaps & Caveats

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; use pow(base, exp).
  • No 0b... binary literals β€” only decimal and 0x... 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 omits Builtins_Buffer.cpp and Builtins_Concurrency.cpp, and references a non-existent Builtins_DB.cpp. Use the manual g++ command above for a complete build.
  • No db_* (SQLite) or pdf_* builtins exist in C++ despite sqlite3 being linked by CMake β€” any database or PDF functionality must come from an ezlib package.
  • No md5, sha256, base64_encode/decode, or hmac_sha256 builtins exist in C++ β€” cryptographic helpers, if available, are ezlib-side.
  • No deleteFile, listDir, fs_exists, getenv, setenv, or exec builtins exist in C++ β€” only readFile/writeFile/appendFile/readLines/writeLine/appendLine.
  • No http_put, http_delete, or startServer C++ builtins β€” only http_get, http_post, and fetch.
  • Windows-only β€” the code directly includes <windows.h> and Win32-specific structures (PE header patching, icon resources, HMODULE/FARPROC FFI), so it cannot be built on Linux/macOS as-is.
  • match arms support only equality comparison against literal/expression patterns plus an other default β€” no ranges, guards, or destructuring.
  • other when is the only valid else-if chain syntax β€” consecutive bare when blocks are independent statements, not an if/else-if/else chain.

🀝 Contributing

Reporting Issues

  • Open an issue on GitHub Issues
  • Include a minimal reproducing .ez script
  • State your Windows version and compiler (MinGW / MSVC)

Pull Requests

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

Code Style

  • 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_*.cpp file via interp.defineGlobal(...) and ensure the file is included in both CMakeLists.txt and any manual build commands

πŸ“„ License

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

⭐ Star on GitHub Β· πŸ› Report a Bug

About

EZ is a lightweight C++ interpreter for a custom scripting language with variables, arrays, loops, conditionals, functions, and I/O, designed for learning compiler and interpreter concepts.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages