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 arex_ts::TypedExpr. - Evaluation (
rexlang-engine): evaluatesTypedExprto a runtimerex_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:rex_parser::Parser::parse_program.- For untrusted code, set
ParserLimits::safe_defaultsbefore parsing.
- For untrusted code, set
rexlang-typesystem: type system. Entry points:TypeSystem::with_prelude()?to create a typing environment with standard types/classes.TypeSystem::infer_typed/TypeSystem::inferfor type inference.- 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::inject_decls(&program.decls)to make user declarations available at runtime.Engine::eval_with_gas(&program.expr, &mut gas).awaitto evaluate.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*) receive&Engine<State>.- Host module injection API:
Module+Export+Engine::inject_module.
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 evaluation:
rexlang-enginealways evaluates aTypedExpr; it typechecks first (viarexlang-typesystem) and then evaluates. This keeps runtime behavior predictable and makes native-function dispatch type-directed. - Prelude split: The type system prelude is a combination of:
- ADT/typeclass heads injected by
TypeSystem::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: 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.
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: 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/module specifiers: import specifiers and path labels are textual before being resolved into structured module 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.