Architecture
Rex is implemented as a small set of focused crates that form a pipeline:
- Parsing (
rex-parser): converts source text into arex_ast::CompilationUnit { decls, body }. - Typing (
rex-typesystem): Hindley–Milner inference + ADTs + type classes; produces arex_typesystem::TypedExpr. - Execution (
rex-engine): builds the host environment, prepares typed code into aCompiledProgram, and evaluates it to a runtimerex_engine::Handle.
The crates are designed so you can use them independently (e.g. parser-only tooling, typechecking-only checks, or embedding the full evaluator).
Crates
rex-ast: shared AST types (Expr,Pattern,Decl,TypeExpr,CompilationUnit, symbols, spans).rex-parser: source parser. Entry point:rex_parser::parse.- Parsing enforces a fixed cap on AST nesting.
rex-typesystem: type system. Entry points:TypeSystem::new()to create an explicit typing environment.infer_typed(&mut ts, expr)/infer(&mut ts, expr)for type inference.- The inference implementation itself lives in
rex-typesystem/src/inference.rs;typesystem.rsnow holds the shared core types, environments, and registration logic. - For untrusted code, set
rex_typesystem::TypeSystemLimits::safe_defaults()before inference.
rex-engine: host environment builder, compiler, and runtime evaluator. Entry points:Builder::with_prelude(state)?to inject runtime constructors and builtin implementations (statecan be()).standard_type_system()?to create a typing environment with therex-enginestandard prelude.Builder::build_compiler()to consume the prepared builder into a compilation view.Compiler::compile_programto consume the compiler and prepare a parsed program entry point into(CompiledProgram, Evaluator).Compiler::infer_*consumes the compiler for type-only checks.Evaluator::run(compiled, inputs).awaitto execute one prepared program.inputsis aBTreeMap<String, Handle>for the program’s externalmaininterface;runconsumes the evaluator, compiled program, and input map.Buildercarries host state asBuilder<State>(State: Clone + Send + Sync + 'static); typedexportcallbacks receive&Stateand returnResult<T, EngineError>, typedexport_asynccallbacks receive&Stateand returnFuture<Output = Result<T, EngineError>>, while handle-based native APIs (export_native*) receiveContext<State>.- compile and evaluation APIs return
EngineError; convenience entry points that cross phases returnExecutionError. - Host module injection API:
Module+Export+Builder::inject_modulefor eager registration, orImporter<State>returningResolvedModuleContent::module(...)for lazy Rust-backed modules.
rex-proc-macro:#[derive(Rex)]bridge for Rust types ↔ Rex ADTs/values.rex: CLI front-end around the pipeline.rex-lsp/rex-vscode: editor tooling.
rex-engine is organized internally around the same phases:
builder/owns builder-facing host/module registration and registry markdown.compiler/owns program preparation, import rewriting, typechecking, module loading state, andCompiledProgramconstruction.evaluator/owns execution, scheduling, native dispatch,Context, and the runtime core.modules/,value.rs, andconfig.rshold shared module identities, heap values/GC roots, and runtime options.
Design Notes
- Typed preparation:
rex-engineprepares code into a typed form before execution. The currentCompiledProgramstores a typed AST plus the environment snapshot needed to run it. - Single-shot execution: evaluation is intentionally one-shot.
CompiledProgramis moved intoEvaluator::runwith its runtime input map, consuming the evaluator as well. Prepare all required declarations/modules before constructing or consuming the evaluator. - Single-use preparation:
Builder::build_compiler(),Compiler::compile_program, and theCompiler::infer_*APIs consume their receivers. Each program run should create a fresh builder/compiler/evaluator lineage. - Same-lineage runtime model: a
CompiledProgramis intended to run on the evaluator produced from the same compiler. Rex programs are supplied as source and compiled per run, so Rex does not expose a portable compiled-artifact or cross-runtime linking model. - Phase ownership:
Builderowns embedder configuration only untilbuild_compiler().Compilerthen owns preparation state: the type environment, runtime declaration environment, runtime registries, heap, module loader caches, and execution policy snapshot.Evaluatoris built only by consuming that compiler, so runtime code cannot mutate modules or compile new declarations. - Prelude ownership:
rex-engineowns the standard prelude source, standard typing environment, and runtime contract. The split is:- typeclass and instance declarations written in Rex at
rex-engine/src/prelude/typeclasses.rex rex-engine/src/prelude/type_system.rsbuilds the prelude-enabledTypeSystem, including ADTs, parsed declarations, and primop schemesrex-engine/src/prelude/mod.rsparses the Rex source and injects runtime method bodies/native implementations forBuilder::with_prelude(state)?rex-typesystemexposes generic registration/inference APIs and does not own the standard prelude
- typeclass and instance declarations written in Rex at
- Depth bounding: Some parts of the pipeline are naturally recursive (parsing deeply nested parentheses, matching deeply nested terms). The parser enforces a fixed AST-depth cap, and the typechecker-limit API provides bounded recursion for production/untrusted workloads.
- Import-use rewrite/validation: module processing resolves import aliases across expression vars, constructor patterns, type references, and class references; unresolved qualified alias members are rejected as module errors before runtime.
- Abstract module namespace: a
ModuleIdis a validated qualified Rex name such asstd.preludeorffmpeg.formats.av1. It does not encode a filesystem path, origin kind, URL, or content hash. Importers decide whether a module name maps to a file, database row, in-memory string, open editor buffer, generated AST, or Rust host module. - Importer payloads and caching: importers are generic over host state and can return Rex
source, a prebuilt
CompilationUnit, or a Rust-backedModule<State>. Source andCompilationUnitmodules are loaded through the SCC module graph path. Rust-backed modules are installed lazily through the same internal module installer used by eagerinject_module, and are self-contained host modules rather than Rex source modules with nested import loading. Importer results are cached for one compile so the same request is not resolved repeatedly.
Intentional String Boundaries
Rex now prefers structured internal representations (for example NameRef, BuiltinTypeId,
CanonicalSymbol, and module/type/class maps) across parser, type system, evaluator, and LSP
rewrite paths. Remaining string usage is intentional in these boundary layers:
- Source text and parsing: the parser accepts source strings by definition.
- Human-facing diagnostics and display: error messages, hover text, CLI rendering, and debug output stringify symbols/types for readability.
- Protocol/serialization boundaries: JSON/LSP payloads are string-based and convert structured internal symbols/types at the edge.
- Module specifiers: parsed import names are textual before being resolved into validated
ModuleIdvalues and handed to importer policy.
Non-goal for this pass:
- Eliminating all
.to_string()calls globally. The design target is to avoid stringly-typed core semantics, not to remove string conversion at UI/protocol boundaries.