Architecture
No bytecode compiler, no VM — a tree-walking interpreter, on purpose.
source (.ezy / .ez)
|
v
Lexer tokens, with INDENT/DEDENT synthesized from
| leading spaces (Python-style layout rule)
v
Parser recursive-descent, one function per grammar
| rule, precedence-climbing for expressions
v
AST plain dataclasses, one per statement/expression kind
|
v
Interpreter tree-walking evaluator: exec_<NodeType> for
| statements, eval_<NodeType> for expressions
v
stdlib/ files.py, process.py, http.py: thin wrappers
around os/shutil, subprocess, and requests
For a scripting language whose workloads are dominated by I/O (file, process, HTTP) rather than tight numeric loops, the overhead of tree-walking is not the bottleneck, and it keeps the whole pipeline — source, tokens, AST, result — inspectable and easy to extend. See Limitations for what this rules out (e.g. a bytecode-level debugger).
Environments
Environment is a parent-linked scope chain: a dict of
names plus a set of names that are const. A function call
creates a new Environment whose parent is the function's
closure — the environment active where the function was
defined — which is how closures work. if/for/
while/try bodies execute directly against the
enclosing environment rather than creating a new one; see "Scoping" in
the language reference for why.
Values
Most Ezy values map directly onto native Python values
(str, int, float,
bool, None, list,
dict) so builtins like parse_json/
to_json need no conversion layer. A handful of small
wrapper types add extra behavior: EzyPath,
HttpResponse, ProcessResult,
Function, BuiltinFunction,
EzyModule, EzyErrorValue.
Control flow
break, continue, and return
are implemented as Python exceptions caught at the appropriate loop or
function boundary — the standard technique for a tree-walking
interpreter, and it keeps each statement's execution method free of
manual "did we return?" flag threading.
Errors
EzyRuntimeError carries an error_type
string ("FileError", "HttpError", ...) and a
message; try/catch catches
exactly this class and binds an error value with those two fields to
the catch variable. A syntax error is a separate class
and is never catchable from inside a script — it's caught before the
program starts running.
String interpolation
While scanning a string literal, the lexer recognizes
{...} and records the enclosed source text rather than
tokenizing it inline. The parser re-lexes and parses that fragment as
a standalone expression with a fresh lexer/parser pair. This keeps the
main grammar free of string-interpolation special cases, at the cost
of a second small pass per interpolated expression — negligible for
realistic script sizes.
Modules
use "./utils" parses and executes the target file once,
in its own top-level environment, then wraps every name it defined
into a namespace and caches it by resolved path so re-importing the
same file within one run is free. Built-in module names
(use "http") are validated against a fixed set but are a
no-op otherwise — the corresponding functions are always available as
globals; use exists to make a script's dependencies
self-documenting and to catch typos.
Processes
Commands are tokenized with shlex.split and run via
subprocess with shell=False, always — see
Security for why. Piping two run
stages together (run "a" -> run "b") wires
Popen stdout directly into the next process's stdin,
rather than buffering the first command's full output before starting
the second.
HTTP
get/post/put/patch/delete expressions and their
with header/query/timeout and send json
modifiers all parse into a single request node and are evaluated in
one place, which keeps the mapping from syntax to the underlying HTTP
call in one spot rather than scattered across the interpreter.