Architecture
Rex is implemented as a small set of focused crates that form a pipeline:
- Lexing (
rexlang-lexer): converts source text into aVec<Token>with spans. - Parsing (
rexlang-parser): converts tokens into arex_ast::expr::Program { decls, expr }. - Typing (
rexlang-typesystem): Hindley–Milner inference + ADTs + type classes; produces arexlang_typesystem::TypedExpr. - Evaluation (
rexlang-engine): evaluatesTypedExprto a runtimerexlang_engine::Value.
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,Program, symbols).rexlang-lexer: tokenizer + spans (Span,Position).rexlang-parser: recursive-descent parser. Entry point:rexlang_parser::Parser::parse_program.- For untrusted code, set
ParserLimits::safe_defaultsbefore parsing.
- For untrusted code, set
rexlang-typesystem: type system. Entry points:TypeSystem::new_with_prelude()?to create a typing environment with standard types/classes.infer_typed(&mut ts, expr)/infer(&mut ts, expr)for type inference.- The inference implementation itself lives in
rexlang-typesystem/src/inference.rs;typesystem.rsnow holds the shared core types, environments, and registration logic. - For untrusted code, set
TypeSystemLimits::safe_defaultsbefore inference.
rexlang-engine: runtime evaluator. Entry points:Engine::with_prelude(state)?to inject runtime constructors and builtin implementations (statecan be()).Engine::compiler()to create a preparation view over that environment.Engine::runtime_env()/Engine::evaluator()to create runtime views over that environment.Compiler::compile_*to prepare source intoCompiledProgram.RuntimeEnv::validate(&compiled)to preflight runtime linkage before execution.Evaluator::run(&compiled, &mut gas).awaitto execute a prepared program.- convenience helpers like
Evaluator::eval_snippetstill exist, but they are just compile-then-run wrappers. Enginecarries host state asEngine<State>(State: Clone + Sync + 'static); typedexportcallbacks receive&Stateand returnResult<T, EngineError>, typedexport_asynccallbacks receive&Stateand returnFuture<Output = Result<T, EngineError>>, while pointer-level APIs (export_native*) receiveEvaluatorRef<'_, State>.- public phase errors are split as
CompileError,EvalError, andExecutionError(for convenience entry points that do both phases). - Host library injection API:
Library+Export+Engine::inject_library.
rexlang-proc-macro:#[derive(Rex)]bridge for Rust types ↔ Rex ADTs/values.rex: CLI front-end around the pipeline.rexlang-lsp/rexlang-vscode: editor tooling.
Design Notes
- Typed preparation:
rexlang-engineprepares code into a typed form before execution. The currentCompiledProgramstill stores a typed AST plus runtime linkage metadata, but the compile/runtime boundary is now explicit in the API. - Current linkage model:
CompiledProgramcaptures the prepared expression and the environment snapshot needed to run it. Rex declarations that are part of the prepared program are captured there. Host-provided exports and typeclass method bindings remain runtime-linked throughRuntimeEnv, which is whyRuntimeEnv::validateexists. So the current model is “prepared plus link-validated”, not “fully self-contained executable artifact”. - Explicit link contract:
CompiledProgram::link_contract()now records the required runtime ABI version and the callable shapes the prepared program expects.RuntimeEnv::capabilities()exposes the matching runtime-side view, and compatibility checks now reject both missing and type-incompatible runtime bindings. - RuntimeEnv split: internally,
RuntimeEnvnow distinguishes the execution snapshot used byEvaluatorand native dispatch from the engine-backed loader state still used by convenience entry points such as library loading and REPL session syncing. That keeps the public model stable while shrinking execution’s implicit dependence on the full engine object. - Process-local boundary:
CompiledProgram::storage_boundary()andRuntimeEnv::storage_boundary()make it explicit that both values still contain process-local state and are not serialization-ready artifacts. - Prelude split: The type system prelude is a combination of:
- ADT/typeclass heads injected by
TypeSystem::new_with_prelude()? - typeclass method bodies (written in Rex) loaded from
rexlang-typesystem/src/prelude_typeclasses.rexand injected byEngine::with_prelude(state)?(statecan be())
- ADT/typeclass heads injected by
- Depth bounding: Some parts of the pipeline are naturally recursive (parsing deeply nested parentheses, matching deeply nested terms). Parser/typechecker limit APIs provide bounded recursion for production/untrusted workloads.
- Import-use rewrite/validation: library processing resolves import aliases across expression vars, constructor patterns, type references, and class references; unresolved qualified alias members are rejected as library errors before runtime.
Intentional String Boundaries
Rex now prefers structured internal representations (for example NameRef, BuiltinTypeId,
CanonicalSymbol, and library/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: lexer/parser operate on 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.
- Filesystem/library specifiers: import specifiers and path labels are textual before being resolved into structured library identities.
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.