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.