Introduction
Welcome to the cppbolt documentation.
cppbolt is a dependency-free, correctness-oriented C++ subset compiler written from scratch in Rust. It compiles standard C++17 source code directly into WebAssembly (.wasm) binary format for instant execution in web browsers, Web Workers, Node.js, and headless test runners.
Try the compiler directly online in the Browser Playground.
Book Navigation
- User Guide:
- Getting Started: Installation, command-line usage, and compiling samples.
- Supported C++ Feature Matrix: Complete matrix of supported C++ constructs, test cases, and known limitations.
- Browser Integration: C ABI, Web Worker integration, and JavaScript host environment.
- Diagnostics & Errors: Diagnostic format, source spans, and catalog of emitted errors.
- Compiler Internals:
- Architecture Overview: The five-stage pipeline and direct AST-to-Wasm lowering model.
- Preprocessor, Lexer, Parser & AST, Semantic Analysis, and WASM Codegen.
- Runtime & Standard Library: Memory layout, free-list allocator, and embedded STL headers.
- Testing System: Corpus runner, variant tests, and native compiler oracle gates.
- Architecture Decision Records:
- Architectural, ABI, and language decisions with documented evidence and trade-offs.
- Contributing Guide:
- Building, running tests, code standards, and PR requirements.
Getting Started with cppbolt
cppbolt is a dependency-free C++ to WebAssembly subset compiler written in Rust. It compiles a defined subset of C++17 directly to WebAssembly bytecode without requiring LLVM, Clang, or Emscripten.
Prerequisites
To build and run cppbolt, you will need:
- Rust Toolchain: Stable Rust with the 2024 edition (
rustcandcargo). - WebAssembly Target (for browser build):
rustup target add wasm32-unknown-unknown. - Node.js (v18+ recommended) for running command-line WebAssembly tests.
- Python 3 (optional, for local HTTP serving of the playground).
- C++ Compiler (optional,
g++orclang++for running native oracle differential tests).
1. Building the Native Compiler
Clone the repository and build the release binary:
# Build the native CLI compiler (target/release/cppbolt-cli)
cargo build --release
The compiled binary will be placed at target/release/cppbolt-cli.
2. Compiling Your First C++ Program
Create a simple C++ program, for example hello.cpp:
#include <iostream>
int main() {
std::cout << "Hello from WebAssembly C++!" << std::endl;
return 0;
}
Compile it to WebAssembly using the CLI:
target/release/cppbolt-cli hello.cpp -o hello.wasm
Running the WebAssembly Module
The emitted .wasm binary imports character, integer, and double output functions along with exit from the host environment env. You can execute it using the repository's Node.js runner:
node tests/test_wasm_compiler.js hello.wasm
Expected output:
Hello from WebAssembly C++!
3. Running the Test Suite
cppbolt includes a corpus test runner that verifies language behavior, golden binary hashes, and error diagnostics:
# Run the testrunner against the release CLI binary
cargo run --bin cppbolt-testrunner -- target/release/cppbolt-cli
The test runner will discover all test groups, verify the manifest in tests/PASSING.txt, and check that all positive tests match their expected output.
4. Running the Browser Playground Locally
You can try the compiler directly in the live Online Playground without local installation, or build and run it locally:
# 1. Build the compiler as a WebAssembly cdylib
cargo build --release --target wasm32-unknown-unknown --lib
# 2. Assemble the playground assets into site/
tools/build_site.sh
# 3. Serve the site locally
python3 -m http.server --directory site 8000
Open http://localhost:8000/ in your browser.
The playground executes compilation inside a background Web Worker so deep compilations never block the main browser thread. You can write C++ code in the browser editor, click Run, and see stdout rendered in real time.
For more details on integrating the compiler into web applications, see Browser Integration.
Supported C++ Feature Matrix
cppbolt implements a defined, correctness-oriented subset of C++17. Every supported feature is covered by automated corpus tests and pinned by cryptographic golden hashes. Features outside the subset are either explicitly rejected with clear diagnostics or tracked as planned future extensions.
The baseline test status is tracked in tests/PASSING.txt. Run the test runner to verify status locally:
cargo run --bin cppbolt-testrunner -- target/release/cppbolt-cli
Feature Matrix
| Feature | Status | Notes | Test |
|---|---|---|---|
Scalar Types (int, long long, short, char, float, double, bool) | Done | ILP32 32-bit data model; unsigned overflow wrapping; binary literals (0b), octal, and single-quote digit separators (') | tests/c_basics/types/arithmetic.cpp, tests/c_basics/types/numeric_literals.cpp, tests/c_basics/types/long_long.cpp, tests/c_basics/types/floats.cpp |
| Pointers & Conversions | Done | 32-bit Wasm i32 addresses; array decay; nullptr (std::nullptr_t); C++17 null pointer constants (0, 0u); multi-level const qualification rules; implicit object pointer to void*; explicit static_cast for void* to typed pointer | tests/c_basics/pointers_arrays/pointers_arrays.cpp, tests/c_basics/types/pointer_conversions.cpp |
| String Literals | Done | Typed as const char[N] with trailing null terminator (sizeof("hello") == 6); decay to const char*; immutable (rejected for char*) | tests/c_basics/types/pointer_conversions.cpp |
| Function Pointers | Done | Indirect calls via WebAssembly function table; comparator callbacks | tests/classes_oop/methods/function_pointer_comparator.cpp |
| Multi-Dimensional Arrays | Done | Nested dimensions wrap inside-out; contiguous row-major layout; pointer-to-array decay | tests/c_basics/pointers_arrays/multidim_arrays.cpp, tests/c_basics/pointers_arrays/multidim_pointer_deref.cpp |
Lvalue References (T&) | Done | Reference binding, parameters, reference return values | tests/classes_oop/statics_namespaces/references.cpp, tests/classes_oop/methods/member_reference_return.cpp |
const Qualifiers | Done | const variables, const pointers, const member methods | tests/c_basics/types/const_qualifiers.cpp, tests/classes_oop/methods/const_methods.cpp |
auto Type Deduction | Done | Local variable and reference type deduction | tests/c_basics/types/auto_type.cpp |
Type Aliases (typedef, using) | Done | Standard typedef and C++11 using Alias = Type; | tests/c_basics/types/auto_type.cpp |
| Multi-Declarator Declarations | Done | Comma-separated declarators sharing base specifiers (int *p, q = 2;) | tests/c_basics/misc/multi_declarator.cpp |
| Array Function Parameters | Done | Parameter adjustment decaying int arr[] and int arr[N] to int* arr | tests/c_basics/pointers_arrays/array_parameter_decay.cpp |
| Enums | Done | Scoped (enum class) with qualified lookup (Scope::Val) and unscoped enum | tests/c_basics/enums/enums.cpp, tests/c_basics/enums/scoped_enums.cpp |
Control Flow (if, switch, loops) | Done | if/else, ternary ?:, while, for, do-while, switch/case/default (with enum & const-expr case labels) | tests/c_basics/control_flow/control_if_else.cpp, tests/c_basics/control_flow/control_loops.cpp, tests/c_basics/control_flow/switch_enum_case.cpp |
| Functions & Overloading | Done | Exact match > reference binding > promotion > conversion; default arguments; global init reachability | tests/c_basics/functions/functions_call.cpp, tests/c_basics/functions/constexpr_global_init_call.cpp, tests/c_basics/functions/overload_resolution.cpp |
| Static Variables | Done | Function-local statics with arguments; class static members | tests/classes_oop/statics_namespaces/function_local_statics.cpp, tests/classes_oop/statics_namespaces/static_members.cpp |
| Structs & Classes | Done | Aggregate init, member variables/methods, access control | tests/classes_oop/structs/struct_basic.cpp, tests/classes_oop/structs/initializer_lists.cpp |
| Constructors & Destructors | Done | Default, parameterized, copy, delegating (C() : C(x) {}); initializer list ordering; implicit synthesis for trivial/empty classes and prvalues (Value().get()); global parenthesized construction | tests/classes_oop/constructors_destructors/copy_semantics.cpp, tests/classes_oop/constructors_destructors/delegating_constructor.cpp, tests/classes_oop/constructors_destructors/prvalue_default_constructor.cpp, tests/classes_oop/statics_namespaces/global_constructor_args.cpp |
| Conversion Operators | Done | User-defined conversion operators operator T() const with explicit qualification checks; disambiguated from copy constructors | tests/classes_oop/operators/conversion_operator.cpp, tests/classes_oop/operators/regress_user_conversion_param.cpp, tests/classes_oop/operators/regress_byval_param_conversion.cpp |
| Ref-Qualified Methods | Done | Lvalue and rvalue ref-qualified member functions (get() &, get() &&) with receiver value-category dispatch | tests/classes_oop/methods/ref_qualified_methods.cpp |
| Structured Bindings | Done | Aggregate decomposition (auto [a, b] = p;, auto&, const auto&) | tests/classes_oop/structs/structured_bindings.cpp |
| RAII Lifecycle & Scope Cleanups | Done | Cleanups on scope exit, early return, break, continue, statement temporaries | tests/classes_oop/memory_raii/class_raii.cpp, tests/classes_oop/memory_raii/raii_early_return.cpp, tests/classes_oop/memory_raii/raii_temporaries.cpp |
| C++17 Copy Elision | Done | Prvalue construction in place (direct init and function return) | tests/classes_oop/memory_raii/prvalue_elision.cpp, tests/classes_oop/memory_raii/direct_initialization_elision.cpp |
Dynamic Memory (new / delete) | Done | Single object and array new[]/delete[]; placement new | tests/classes_oop/memory_raii/new_delete.cpp, tests/classes_oop/memory_raii/new_delete_object_arrays.cpp, tests/classes_oop/memory_raii/placement_new.cpp |
| Single & Linear Inheritance | Done | Base subobjects, slicing, constructor/destructor chains | tests/classes_oop/inheritance/single_inheritance.cpp, tests/classes_oop/inheritance/multi_level_inheritance.cpp |
| Virtual Functions & Polymorphism | Done | Vtable dispatch, virtual destructors, pure virtual (= 0) | tests/classes_oop/inheritance/virtual_functions.cpp, tests/classes_oop/inheritance/virtual_destructor.cpp, tests/classes_oop/inheritance/pure_virtual.cpp |
| Namespaces | Done | Nested namespaces, using namespace, qualified lookups | tests/classes_oop/statics_namespaces/namespaces_comprehensive.cpp |
| Operator Overloading (Binary & Compound) | Done | +, -, *, /, %, ==, !=, <, <=, >, >=, <<, >>, &, ^, |, =, +=, -=, *= | tests/templates_operators/op_overload_binary.cpp, tests/classes_oop/operators/chained_compound_assign.cpp |
| Operator Overloading (Prefix & Subscript) | Done | Unary +, -, !, ~, prefix ++/--, subscript [], operator bool() | tests/templates_operators/op_overload_unary.cpp, tests/templates_operators/op_overload_subscript.cpp, tests/classes_oop/operators/contextual_bool_comprehensive.cpp |
| Preprocessor | Done | #define (function/object, #, raw ##), #undef, #include, #if/#elif, #error, #warning, \ | tests/c_basics/preprocessor/preprocessor.cpp, tests/c_basics/preprocessor/macro_rescan.cpp, tests/c_basics/preprocessor/macro_token_paste_raw.cpp, tests/c_basics/preprocessor/warning_directive.cpp |
C Standard I/O (<cstdio>) | Done | printf (all conversions %d, %i, %u, %o, %x, %X, %c, %s, %e, %E, %f, %g, %G), sprintf, snprintf, puts | tests/c_basics/io_printf/printf_conversions.cpp, tests/c_basics/io_printf/printf_table.cpp, tests/mini_stl/cstdio_basic.cpp |
C++ Streams (<iostream>) | Done | std::cout, std::cin, std::getline, custom stream operators | tests/mini_stl/iostream_basic.cpp, tests/mini_stl/iostream_cin.cpp, tests/mini_stl/stream_operator_overload.cpp |
Standard String (<string>) | Done | std::string, .c_str(), .length(), .append(), concatenation | tests/mini_stl/string_basic.cpp, tests/mini_stl/string_comprehensive.cpp |
Math & Formatting (<cmath>, <iomanip>) | Done | Math intrinsics, std::abs integer overloads for long and long long, std::setprecision, std::fixed, std::setw | tests/mini_stl/cmath_basic.cpp, tests/c_basics/functions/std_abs_long.cpp, tests/mini_stl/iostream_precision_after_fixed.cpp |
Templates & Member Templates (template <typename T>) | Done | Class templates, out-of-class member method definitions, member function templates, dependent member type aliases, lazy method instantiation, multi-arity function overloads, and deduction for callables and pointers | tests/templates_operators/template_class_basic.cpp, tests/templates_operators/template_class_methods.cpp, tests/templates_operators/template_func_basic.cpp, tests/templates_operators/template_multi_param.cpp, tests/templates_operators/template_pair.cpp, tests/mini_stl/pair_basic.cpp |
Containers: Vector & Utility (<vector>, <utility>) | Done | std::vector (dynamic growth, capacity doubling, element access, iterators, brace initialization, push_back self-aliasing protection, RAII lifecycle cleanup), std::pair and std::make_pair | tests/mini_stl/vector_basic.cpp, tests/mini_stl/vector_brace_init.cpp, tests/mini_stl/vector_objects.cpp, tests/mini_stl/vector_aliasing.cpp, tests/mini_stl/iterators_range_for.cpp, tests/mini_stl/combined_integration.cpp |
Algorithms (<algorithm>) | Done | std::sort (default comparator and custom functor), std::reverse, std::min, std::max, std::swap | tests/mini_stl/algorithm_basic.cpp, tests/mini_stl/algorithm_sort.cpp, tests/mini_stl/sort_comparator.cpp |
Associative Containers (<map>, <set>) | Done | Balanced binary search tree (detail::AVLTree), bidirectional iterators, insertion, lookup, range traversal, deletion, and differential fuzzing | tests/mini_stl/map_basic.cpp, tests/mini_stl/set_basic.cpp, tests/mini_stl/differential_fuzz.cpp |
| Nested Classes & Structs | Done | Member classes in non-template and template classes, access control validation, fixed-point dependent type alias resolution, and scoped member registration | tests/classes_oop/structs/nested_classes.cpp, tests/classes_oop/structs/nested_classes_scopes.cpp, tests/templates_operators/template_nested_classes.cpp, tests/templates_operators/template_nested_alias_chain.cpp |
Dependent Types (typename) | Done | Disambiguation of dependent member types (typename T::Type), rejection of unqualified typename | tests/templates_operators/typename_dependent_syntax.cpp, tests/negative/typename_unqualified.cpp |
Range-Based for Loops | Done | C++11 for (decl : range) over fixed-size arrays (1D and multi-dimensional) and classes with .begin()/.end(); supports auto, const auto, auto&, const auto& | tests/c_basics/pointers_arrays/range_for_array.cpp, tests/mini_stl/range_for_string.cpp |
Overloaded Postfix ++ / -- | Done | Overloaded operator++(int) and operator--(int) distinguish postfix from prefix via dummy int parameter | tests/classes_oop/operators/operator_postfix_incdec.cpp |
Overloaded ->, *(), () | Done | Smart-pointer and functor operators: operator-> (recursive chaining with cycle detection), dereference operator*(), and function call operator() | tests/classes_oop/operators/operator_smart_pointer.cpp, tests/classes_oop/operators/operator_arrow_chaining.cpp, tests/classes_oop/operators/operator_call_functor.cpp |
| Lambda Expressions (C++17) | Done | [], [x], [&x], [=], [&], mutable, trailing return type (-> T), return type deduction, auto storage, direct invocation, unary + decay, and implicit conversion of captureless lambdas to C function pointers (R (*)(P...)) including return targets | tests/classes_oop/lambdas/lambda_basic.cpp, tests/classes_oop/lambdas/lambda_default_captures.cpp, tests/classes_oop/lambdas/lambda_function_pointer.cpp, tests/classes_oop/lambdas/lambda_return_fn_ptr.cpp, tests/classes_oop/lambdas/lambda_range_for_capture.cpp, tests/classes_oop/lambdas/lambda_unary_plus.cpp |
| List Initialization & Aggregates | Done | Direct-list (T x{...}), copy-list (T x = {...}), empty brace value initialization ({}), namespace-scope (global) brace initialization, aggregate base subobjects, brace elision, narrowing conversion checks ([dcl.init.list] p7), dynamic allocation (new T{...}, new T[n]{...}), typed temporaries (Type{...}.get()), and explicit constructor enforcement | tests/classes_oop/structs/brace_init_scalars.cpp, tests/classes_oop/structs/brace_init_classes.cpp, tests/classes_oop/structs/brace_init_aggregates.cpp, tests/classes_oop/structs/brace_init_brace_elision.cpp, tests/classes_oop/structs/brace_init_temporaries.cpp, tests/classes_oop/structs/brace_init_dynamic_new.cpp |
std::initializer_list | Done | Compiler-created contiguous backing arrays, begin()/end()/size(), constructor preference for braced containers (ranked by exact, promotion, and standard conversions), empty-brace fallback to an initializer-list constructor, nested brace lists, namespace-scope lists, auto deduction, homogeneity checks, narrowing diagnostics, and parenthesized braced arguments | tests/mini_stl/initializer_list_basic.cpp, tests/mini_stl/vector_brace_init.cpp, tests/mini_stl/initializer_list_ctor_ranking.cpp, tests/mini_stl/initializer_list_nested.cpp, tests/mini_stl/initializer_list_global.cpp, tests/negative/initializer_list_narrowing.cpp, tests/negative/initializer_list_mismatched_types.cpp |
Const-Ref Method Overloading (T m() const &) | Planned | const methods supported; const & method ref-qualifiers are rejected with const overloading is not supported | tests/negative/const_overload_unsupported.cpp |
Namespace Aliases (namespace A = B;) | Done | C++17 namespace alias declarations at namespace, file, and block scope with chained and qualified resolution | tests/classes_oop/statics_namespaces/namespace_aliases.cpp, tests/negative/namespace_alias.cpp |
Extended Casts (reinterpret_cast) | Done | C++17 reinterpret_cast<T>(expr) across scalar integers, pointers, and reference types within the ILP32 data model; bit-preserving conversions between integers and pointers, pointer conversions without adjusting offsets, and byte inspection; disallows scalar float conversions | tests/c_basics/types/reinterpret_cast.cpp |
| Functional Casts & Value Initialization | Done | Value initialization of scalars and typedef aliases (T(), zero-initialized) and explicit functional conversions (Type(val)) | tests/c_basics/types/value_init_scalar.cpp |
| Scalar Pseudo-Destructors | Done | No-op pseudo-destructor calls on scalar types (p->~T(), x.~T()), preserving receiver evaluation and side effects | tests/c_basics/types/pseudo_destructor_scalar.cpp |
| Destructors via Typedef Aliases | Done | Explicit destructor calls resolved through typedef aliases (sp->~ClassT()), verifying target class identity | tests/classes_oop/constructors_destructors/destructor_via_typedef.cpp |
| Multiple & Virtual Inheritance | Out of Scope | Single/linear inheritance supported; multiple and virtual base specifiers are rejected | tests/negative/multiple_inheritance.cpp, tests/negative/virtual_inheritance.cpp |
Exceptions (throw) | Diagnosed | throw keyword recognized and rejected with explicit target diagnostic (C++ exception handling ('throw') is not supported on this WebAssembly target) | tests/negative/throw_unsupported.cpp |
Known Limitations & Design Rules
1. Language Semantics & Edge Cases
- Copy Elision: Prvalue initialization is elided as guaranteed by C++17 (
T x = T(1);). However, return of named local variables requires running the copy constructor. - Null Pointer Constants & Pointer Conversions:
- Integer literal zeroes (
0,0u) andnullptr(std::nullptr_t,decltype(nullptr)) qualify as null pointer constants. Zero-valued variables, non-zero integers, andfalseare rejected. - Qualification conversions require every subsequent indirection level to be
constwhen addingconstto an intermediate level (int**converts toconst int* const*, but notconst int**). - Object pointers convert implicitly to
void*; convertingvoid*to a typed pointer requiresstatic_cast<T*>.
- Integer literal zeroes (
- String Literal Semantics: String literals have type
const char[N]with a null terminator (sizeof("hello") == 6). They decay toconst char*for call arguments and pointer assignments, and cannot bind tochar*. - Multiple and Virtual Inheritance: Explicitly rejected at parse time:
- Second base specifier yields:
multiple inheritance is not supported virtualbase specifier yields:virtual inheritance is not supported
- Second base specifier yields:
- Operator Overloading Boundaries: User-defined operator overloads for arithmetic, relational, bitwise, assignment, subscript
[], smart-pointer->, dereference*(), functor(), postfix++/--, and conversion operatorsoperator T()are supported. Method ref-qualifier overloading with const-lvalue references (const &) is explicitly rejected withconst overloading is not supported. - Chained
operator->& Cycle Detection: In accordance with C++17, member access via->recursively resolves through chainedoperator->()overloads returning class objects or references until a raw struct pointer is reached. Cycle detection prevents compile-time recursion loops (circular operator-> delegation on '<class>'), and recursion depth is budgeted to 128 hops. - C++17 Lambda Expressions: Lambdas desugar into anonymous closure structs (
__lambda_N) holding captured state as either by-value copies or references. The synthesizedoperator()isconst-qualified unless markedmutable, enforcing immutability on by-value captures. Return types can be explicitly annotated with trailing return syntax (-> T) or automatically deduced fromreturnstatements. Captureless lambdas ([]) synthesize a static__invoketrampoline enabling implicit conversion to standard C function pointers (R (*)(P...)), including as return statement targets and via unary+decay (+[](){}). Default captures (=,&) must strictly appear first in the capture list, and free-variable discovery traversesRangeFor,StructuredBinding, and nestedLambda/Newconstructs. - List Initialization & Aggregates: Braced initializer lists (
{...}) work across variable declarations, namespace-scope (global) variables, constructor member initializers (: member{val}), returns, call arguments, assignments, typed temporaries (Type{args...}.member()), and heap allocations (new T{...},new T[n]{...}).- Direct-list initialization allows selecting
explicitconstructors; copy-list initialization rejects them. Empty braces ({}) value-initialize scalars and enums to zero, clear aggregate fields, or call default constructors. - Under C++17 aggregate rules, classes qualify as aggregates when they have no user-declared constructors, no private or protected non-static data members, no virtual functions, and no virtual or private base classes. Public non-virtual bases initialize as initial subobjects, either with nested braces (
Derived{{base_args}, derived_args}) or flattened across the outer list. - Narrowing conversion checks follow C++17 [dcl.init.list] p7, rejecting conversions from floating-point to integer,
doubletofloat(unless an exact constant literal), and between integer types with sign changes or smaller ranges. Compile-time constant expressions, including negative integer literals, are accepted when the value fits the destination type. - Single-object
new T{...}direct-initializes the heap object. Arraynew T[n]{...}initializes explicit elements and runs a WebAssembly loop to value-initialize omitted trailing elements up ton. - Initializer-list constructors taking
std::initializer_list<T>receive a frame-backed pointer-and-size view. The backing array stays live through the full expression; elements of local initializer-list variables are destroyed at scope exit. - Multiple viable initializer-list constructors are ranked by element conversions: exact matches beat integral promotions, which beat other standard conversions. A narrowing candidate is discarded, and its diagnostic is reported only when no viable candidate remains. Empty braces use the initializer-list constructor when the class has no default constructor.
- Nested brace lists such as
std::vector<std::vector<int>> v{{1, 2}, {3, 4}}are resolved against the chosen element type after constructor selection. - Namespace-scope lists and braced containers are initialized before
mainruns. The backing array of a namespace-scopestd::initializer_listis placed inmain's frame, which outlives every use of the list.
- Direct-list initialization allows selecting
- Extended Casts (
reinterpret_cast): Supports C++17reinterpret_cast<T>(expr)between pointer types, pointer and integer types (matching the 32-bit ILP32 pointer width), and reference types (reinterpret_cast<T&>(v)). Does not invoke user-defined conversion operators or constructors. Scalar floating-point reinterpretations by value are rejected; bit patterns of floats can be inspected via pointer or reference reinterpretation (e.g.*reinterpret_cast<uint32_t*>(&f)). - Template Core:
- Class templates and function templates support type parameters (
template <typename T>). Non-type parameters, template template parameters, variadic templates, and explicit specializations (template <>) are rejected with explicit diagnostics. - Instantiation follows a single-phase lookup model (ADR 0010). The parser preserves template declarations in the AST (
TemplateDecl), and semantic analysis resolves names and types during concrete specialization. - Type resolution recursively unwraps pointers, references, const qualifiers, and arrays, generating concrete struct layouts before calculating member access offsets.
- Non-template structs compute layouts after resolving template fields, giving fields valid offsets and sizes. Non-template functions accept and return template instance types.
- Class template member methods instantiate lazily on first call, address-of reference, or operator overload lookup (
operator[], unary operators). Unused member methods are neither typechecked nor emitted into WebAssembly. - Non-virtual class template destructors instantiate eagerly alongside virtual methods so synthesized cleanup logic can drop values on scope exit. Dynamically instantiated template classes containing fields with non-trivial destructors synthesize cleanup methods and copy operations.
- Function template overload resolution deduces type arguments from values, references, const references, top-level const pointers, and template instance arguments. Candidate registration tracks overloads sharing identical parameter counts by signature. Explicit template arguments (
func<int>(val),func<Inner<int>>(val)) override deduction and parse nested template types. Default argument expressions fill omitted trailing parameters for both free and member template functions. Exact non-template function overloads take precedence over template specializations. - Out-of-class method definitions match class template and concrete struct declarations by parameter types and const qualification, including across multi-part namespace paths (
Outer::Inner::Class::Class()). - Member template parameters shadow outer class template parameters with identical names during class instantiation.
- Expression calls on struct instances dispatch to
operator()only when an explicit call operator or member templateoperator()exists. - Nested class declarations inside class templates clone and specialize on outer template instantiation, generating qualified types (such as
Container<int>::Node), computing distinct field layouts, and registering member methods. - Nested class type specifiers (
Outer::Inner) enforce access control during type resolution. Private nested types can only be referenced from within the enclosing class, its member methods, or template specializations parameterized with the nested type. Referencing private nested types externally triggers a compile diagnostic (tests/negative/nested_type_private_access.cpp). Nested class short names remain scoped to the outer struct and do not pollute the global namespace (tests/negative/nested_class_unqualified_leak.cpp). - Chained dependent type aliases resolve iteratively to a fixed point during template specialization, supporting arbitrary alias chains without fixed loop bounds (
tests/templates_operators/template_nested_alias_chain.cpp). Member type aliases in thestdnamespace are restricted to templates actually defined withinstd. - Dependent member type specifiers require the
typenameprefix (typename Container<T>::iterator). Unqualifiedtypenameor non-type usage is rejected at parse time. - Instantiation recursion is limited to 500 levels (
template instantiation depth exceeded (limit: 500)).
- Class templates and function templates support type parameters (
2. Nesting Limits per Construct
To protect WebAssembly engine shadow stacks from exhaustion when compiling deep programs in Web Workers, the compiler enforces per-construct nesting budgets (src/depth.rs). One construct cannot exhaust the budget of another:
| Construct | Limit | Deepest Accepted Input (Native & Wasm) |
|---|---|---|
| Expressions | 4,000 | Parenthesized conditional costs 2 levels |
| Parentheses | 1,600 | 1,600 nested parentheses |
| Call Argument Lists | 1,500 | 1,500 nested calls |
| Statements | 1,600 | 1,599 nested if statements |
| Compound Blocks | 1,600 | 1,599 nested {} blocks |
| Declarations | 1,600 | 1,600 nested declarations |
| Type Expressions | 1,600 | 1,600 nested type qualifiers |
| Preprocessor Conditions | 1,600 | 1,600 nested #if conditions |
| Include Depth | 128 | 128 nested #include directives (129 is rejected) |
These limits are verified by tests/nesting.rs and tests/wasm_stack_safety.rs.
Wide left-associative expressions (sums of 20,000 terms), string concatenations (4,000 terms), and else-if chains (5,000 branches) are flattened iteratively and do not consume recursion depth.
3. Printf Runtime Specifications
The embedded printf engine (src/runtime/format.rs) supports:
- Specifiers:
%d,%i,%u,%o,%x,%X,%c,%s,%e,%E,%f,%g,%G. - Modifiers:
h,hh,l,ll, andz. - Flags:
0(zero-pad),-(left-align),+(sign),(space sign),#(alternate form). - Dynamic precision:
.*is supported. (Dynamic width*is not implemented). - Unsupported conversions:
%pand%n. - NaN and Infinities: Zero padding falls back to spaces; sign of NaN is implementation-defined.
Browser Integration & Host Interface
cppbolt is designed to run both natively and inside modern WebAssembly environments (browsers and Node.js). To achieve complete independence from external runtimes like Emscripten, cppbolt defines two clear interfaces:
- Compiler C ABI: Exported functions that allow JavaScript host code to pass C++ source strings into the compiler WebAssembly instance and retrieve compiled WebAssembly binaries or error messages.
- Execution Host Interface (
npm/src/host.ts): Import functions required by compiled WebAssembly programs to perform console I/O, format numbers, and exit. The browser/Node copyweb/host.jsis generated from this single TypeScript source.
1. Compiler WebAssembly C ABI
When built with --target wasm32-unknown-unknown --lib, cppbolt exports a low-level C ABI implemented in src/ffi.rs. Memory management is explicit: each compilation result is an independent boxed slice that must be allocated and released through matching functions.
Exported Functions
| Export | Signature | Description |
|---|---|---|
alloc_buffer | (size: i32) -> i32 | Allocates an uninitialized input buffer of size bytes in the compiler's linear memory. |
dealloc_buffer | (ptr: i32, size: i32) -> void | Frees an allocated input buffer if compilation is aborted before calling compile_wasm. |
compile_wasm | (ptr: i32, len: i32) -> i32 | Compiles UTF-8 source text. Returns a pointer to the emitted .wasm binary on success, or 0 on error. |
get_result_len | () -> i32 | Returns the byte length of the successful compilation result. |
free_result | (ptr: i32, len: i32) -> void | Releases the compiled result buffer back to the compiler allocator. |
get_error_ptr | () -> i32 | Returns a pointer to the static null-terminated error string if compilation failed. |
get_error_len | () -> i32 | Returns the byte length of the static error string. |
JavaScript Embedding Example
// Step 1: Instantiate the compiler WebAssembly module
const compilerBytes = await fetch("pkg/cppbolt_bg.wasm").then(r => r.arrayBuffer());
const { instance } = await WebAssembly.instantiate(compilerBytes, {});
const wasm = instance.exports;
// Step 2: Encode source text and copy into compiler memory
const sourceText = 'int main() { return 42; }';
const encoded = new TextEncoder().encode(sourceText);
const inputPtr = wasm.alloc_buffer(encoded.length);
new Uint8Array(wasm.memory.buffer, inputPtr, encoded.length).set(encoded);
// Step 3: Run compilation
const resultPtr = wasm.compile_wasm(inputPtr, encoded.length);
if (resultPtr === 0) {
// Compilation failed: read static error message
const errLen = wasm.get_error_len();
const errPtr = wasm.get_error_ptr();
const errBytes = new Uint8Array(wasm.memory.buffer, errPtr, errLen);
const message = new TextDecoder().decode(errBytes);
console.error("Compilation error:\n" + message);
} else {
// Compilation succeeded: copy out emitted WebAssembly bytecode
const resultLen = wasm.get_result_len();
const outputBytes = new Uint8Array(wasm.memory.buffer, resultPtr, resultLen).slice();
// Explicitly release the result buffer
wasm.free_result(resultPtr, resultLen);
// outputBytes is now a complete, standalone .wasm binary ready to run!
}
The error buffer is private static storage owned by the compiler; failed compilations never leak linear memory. Bounded memory consumption across repeated compilations is verified by tests/ffi_lifecycle.js.
2. Program Execution Host Runtime (web/host.js)
When a compiled C++ program runs, its WebAssembly module expects an imported env namespace providing standard character and numeric I/O. The single implementation lives in npm/src/host.ts; tools/build_web_host.sh compiles it into web/host.js for the browser playground, and tools/build_site.sh runs that step automatically.
// web/host.js exposes createWasmHost as a browser global and as a CommonJS export.
const host = createWasmHost({
stdin: "", // string, or () => number for streaming input
onStdout: (text) => console.log(text), // streaming output hook
onExit: (code) => console.log("exit", code),
});
// host.imports provides the imports needed by the compiled C++ program
const program = await WebAssembly.instantiate(outputBytes, host.imports);
program.instance.exports.main();
// Everything the program wrote so far
console.log(host.getStdout());
Imported Environment Functions
| Import | Signature | Behavior |
|---|---|---|
env.print_char | (ch: i32) -> void | Writes a single ASCII/UTF-8 character byte to stdout buffer. |
env.print_int | (val: i64, radix: i32, width: i32, prec: i32, flags: i32) -> i32 | Formats integer with specified width, precision, and flags. Returns character count. |
env.print_double | (val: f64, width: i32, prec: i32, mode: i32, flags: i32) -> i32 | Formats double in general (0), fixed (1), or scientific (2) mode. Returns character count. |
env.read_char | () -> i32 | Reads next character byte from stdin buffer (returns -1 on EOF). |
env.exit | (code: i32) -> void | Terminates execution with the given status code. |
Formatting Flags Bitmask
For print_int and print_double:
- Bit 0 (
0x01): Zero-padding ('0'). - Bit 1 (
0x02): Left-alignment ('-'). - Bit 2 (
0x04): Always show sign ('+'). - Bit 3 (
0x08): Space sign (' '). - Bit 4 (
0x10): Uppercase hex/exponent ('X','E'). - Bit 5 (
0x20): Unsigned integer conversion (print_intonly). - Bit 6 (
0x40): Alternate format ('#').
3. Web Worker Architecture in the Playground
Compiling complex C++ programs with deep recursion can consume significant CPU cycles. To ensure smooth browser UI responsiveness, the browser playground (web/playground.html) offloads all compiler work to a Web Worker (web/playground-worker.js):
Playground Page (UI) Web Worker (Compiler) Compiled C++ Instance
│ │ │
│── postMessage({compile, code}) ─>│ │
│ │── compile_wasm(ptr, len) │
│ │ │
│<─ postMessage({success, bytes}) ─│ │
│ │
│── WebAssembly.instantiate(wasmBytes, hostEnv) ─────────────────────>│
│── exports.main() ──────────────────────────────────────────────────>│
│ │
│<── host.print_char / print_int (stdout) ────────────────────────────│
│ │
Shareable URLs
The playground supports URL query parameters so interactive code snippets can be shared:
code: URL-encoded C++ source code.run=1: Automatically compiles and executes the program upon loading without requiring a button click.compiler: Custom URL path to acppbolt_bg.wasmcompiler binary.
Example:
http://localhost:8000/?run=1&code=int%20main()%20%7B%20return%200%3B%20%7D
Execution flow in Node.js and headless browser flows is covered by tests/browser_flow.rs.
Diagnostics & Compiler Errors
All diagnostic messages in cppbolt are represented by the unified CompileError struct. Error formatting follows GCC and Clang conventions:
file.cpp:line:column: error: message
Diagnostics never expose Rust internal debug formatting (Debug) or panics.
1. Diagnostic Architecture
Source Coordinate Fidelity
- Preprocessor Origins: When files are included via
#include, the preprocessor records line origin metadata (src/preprocessor/lines.rs). Errors originating from header files report the path and line number of the header itself rather than the combined translation unit. - Token Spans: The lexer attaches a precise
Span(FileId, line, column, length) to every emitted token. - Expression Precision: Every AST expression node stores an
ExprPosholding the exact line and column of its initial token. Diagnostics for invalid member accesses, dereferences, or conversions point directly at the offending expression. - Statement Lowering Coordinates: Code generation errors that cannot name a sub-expression report the source position of the parent statement.
The corpus test tests/diagnostic_positions.rs compiles every corpus file and enforces that no diagnostic reports a fabricated line or column.
2. Diagnostic Categories (ErrorKind)
The compiler categorizes diagnostics into five distinct kinds:
#![allow(unused)] fn main() { pub enum ErrorKind { Lexical, // Tokenization and preprocessor syntax errors Syntax, // Parser grammar failures Semantic, // Type checking, symbol resolution, and layout failures Unsupported, // Features outside the v1 C++ subset Internal, // Internal compiler invariants or resource limits } }
3. Catalog of Common Diagnostics & Trigger Examples
The following catalog lists diagnostics emitted by the compiler, paired with real triggers from tests/negative/:
Unsupported C++ Features (ErrorKind::Unsupported)
multiple inheritance is not supported
- Cause: A class declaration attempts to inherit from more than one base class.
- Trigger (
tests/negative/multiple_inheritance.cpp):struct A {}; struct B {}; struct C : public A, public B {}; // error: multiple inheritance is not supported
virtual inheritance is not supported
- Cause: A base class specifier includes the
virtualkeyword. - Trigger (
tests/negative/virtual_inheritance.cpp):struct Base {}; struct Derived : virtual public Base {}; // error: virtual inheritance is not supported
range expression must be an array or class with begin() and end()
- Cause: Attempting a range-based
forloop on a scalar type or a class lacking.begin()and.end()methods. - Trigger (
tests/negative/range_for_non_iterable.cpp):int main() { int total = 0; for (int x : 42) { // error: range expression must be an array or class with begin() and end(), found 'int' total += x; } return total; }
Arrow member access on non-pointer
- Cause: Using
->on a struct or primitive variable that does not define an overloadedoperator->(). - Trigger (
tests/negative/operator_arrow_invalid.cpp):struct Dummy { int val; }; int main() { Dummy d(1); return d->val; // error: Arrow member access on non-pointer }
circular operator-> delegation on '<class>'
- Cause: Recursive
operator->()chaining encounters a cyclical delegation loop (e.g.A -> B -> AorA -> A). - Trigger (
tests/negative/operator_arrow_cycle.cpp):struct B; struct A { B operator->(); }; struct B { A operator->(); }; int main() { A a; return a->val; // error: circular operator-> delegation on 'A' }
operator-> must return a pointer to a struct or class
- Cause: Overloaded
operator->()returns a primitive or non-pointer type instead of a class pointer or wrapper object. - Trigger (
tests/negative/operator_arrow_non_pointer.cpp):struct Bad { int operator->() { return 42; } }; int main() { Bad b; return b->val; // error: operator-> must return a pointer to a struct or class }
reference to '<name>' is ambiguous
- Cause: An unqualified identifier is provided by two active
using namespacedirectives or using declarations with equal precedence. - Trigger (
tests/negative/ambiguous_using_directives.cpp):namespace A { int shared = 1; } namespace B { int shared = 2; } using namespace A; using namespace B; int main() { return shared; } // error: reference to 'shared' is ambiguous
call of overloaded '<name>(...)' is ambiguous
- Cause: Overload resolution finds multiple candidate functions with identical ranking scores.
- Trigger (
tests/negative/ambiguous_overload_call.cpp):void pick(float); void pick(long long); int main() { pick(1.0); // error: call of overloaded 'pick(double)' is ambiguous }
lvalue required as left operand of assignment
- Cause: Attempting to assign a value to an rvalue expression or temporary.
- Trigger (
tests/negative/assign_to_rvalue.cpp):int main() { 1 = 2; // error: lvalue required as left operand of assignment }
Cannot assign to variable '<name>' with const-qualified type
- Cause: Mutating a variable declared with
const. - Trigger (
tests/negative/const_assign.cpp):int main() { const int c = 5; c = 6; // error: Cannot assign to variable 'c' with const-qualified type }
no matching conversion for argument N of '<func>': cannot convert '<type1>' to '<type2>'
- Cause: Passing an argument whose type cannot be converted to the parameter type through standard promotions or converting constructors.
- Trigger (
tests/negative/function_int_to_struct.cpp):struct Other { Other(const Other&); }; void takes_struct(Other); int main() { takes_struct(1); // error: no matching conversion for argument 1 of 'takes_struct': cannot convert 'int' to 'Other' }
no matching constructor for initialization of '<class>'
- Cause: Creating an object where provided arguments do not match any constructor overload.
- Trigger (
tests/negative/constructor_too_many_arguments.cpp):struct T { T(int); }; int main() { T x(1, 2); // error: no matching constructor for initialization of 'T' }
invalid operands to binary expression: two pointers cannot be added
- Cause: Applying the binary
+operator between two pointer operands. - Trigger (
tests/negative/pointer_plus_pointer.cpp):int main() { int a = 1, b = 2; return (&a + &b) != 0; // error: invalid operands to binary expression: two pointers cannot be added }
'<member>' is a private member of '<class>'
- Cause: Accessing a private struct or class member from outside its member methods.
- Trigger (
tests/negative/private_access.cpp):class Secret { int hidden; }; int main() { Secret s; return s.hidden; // error: 'hidden' is a private member of 'Secret' }
Variable type '<class>' is an abstract class
- Cause: Instantiating an object of a class containing un-overridden pure virtual functions (
= 0). - Trigger (
tests/negative/pure_virtual_instantiation.cpp):struct Abstract { virtual void run() = 0; }; int main() { Abstract a; // error: Variable type 'Abstract' is an abstract class }
narrowing conversion from '<type1>' to '<type2>' inside braced-init-list
- Cause: Converting between incompatible types inside a braced initializer list (e.g. floating-point to integer).
- Trigger (
tests/negative/narrowing_float_to_int.cpp):int main() { int x{3.14}; // error: narrowing conversion from 'double' to 'int' inside braced-init-list return x; }
narrowing conversion of constant <val> from '<type1>' to '<type2>' inside braced-init-list
- Cause: Converting a constant expression or literal to an integer type whose representable range cannot accommodate the value.
- Trigger (
tests/negative/narrowing_constant_overflow.cpp):int main() { char c{300}; // error: narrowing conversion of constant 300 from 'int' to 'char' inside braced-init-list return c; }
chosen constructor for '<class>' is explicit in copy-list-initialization
- Cause: An
explicitconstructor is matched during copy-list-initialization (Type x = {...},return {...},fn({...})). - Trigger (
tests/negative/explicit_ctor_copy_list.cpp):struct Foo { explicit Foo(int) {} }; int main() { Foo f = {42}; // error: chosen constructor for 'Foo' is explicit in copy-list-initialization return 0; }
scalar initializer has multiple expressions
- Cause: Initializing a scalar or pointer variable with a braced list containing more than one element.
- Trigger (
tests/negative/scalar_excess_elements.cpp):int main() { int x{1, 2}; // error: scalar initializer has multiple expressions return x; }
too many initializers for aggregate '<class>'
- Cause: Providing more initializer elements in a braced list than the target aggregate class has fields or subobjects.
- Trigger (
tests/negative/aggregate_excess_elements.cpp):struct Point { int x; int y; }; int main() { Point p{1, 2, 3}; // error: too many initializers for aggregate 'Point' return 0; }
excess elements in array initializer: expected at most <N>, have <M>
- Cause: A dynamic array allocation
new T[N]{...}specifies more initializer expressions than the declared array boundN. - Trigger (
tests/negative/new_array_excess_elements.cpp):int main() { int* p = new int[2]{1, 2, 3}; // error: excess elements in array initializer: expected at most 2, have 3 return 0; }
cannot initialize variable '<name>' of type <target> with <source>
- Cause: Initializing a typed variable with an incompatible expression, such as assigning a non-zero integer or
boolto a pointer, discardingconstqualifiers, violating multi-level pointer qualification rules, or assigning a string literal to a non-constchar*. - Trigger (
tests/negative/pointer_conversion_int_nonzero.cpp):int main() { int* p = 42; // error: cannot initialize variable 'p' of type int * with int return 0; }
Preprocessor & Lexical Errors (ErrorKind::Lexical)
Invalid digit '<digit>' in binary literal
- Cause: Non-binary digit in a binary integer literal (
0b...). - Trigger (
tests/negative/invalid_binary_digit.cpp):int main() { int x = 0b102; // error: Invalid digit '2' in binary literal return x; }
Invalid digit separator
- Cause: Consecutive digit separators or quotes at literal boundaries.
- Trigger (
tests/negative/invalid_digit_separator.cpp):int main() { int x = 1''000; // error: Invalid digit separator return x; }
Invalid digit '<digit>' in octal literal
- Cause: Non-octal digit (
8or9) in a zero-prefixed octal literal. - Trigger (
tests/negative/invalid_octal_digit.cpp):int main() { int x = 088; // error: Invalid digit '8' in octal literal return x; }
include cycle detected: '<path>'
- Cause: Cyclic
#includeinclusion chain detected by the preprocessor. - Trigger (
tests/negative/include_cycle.cpp):#include "include_cycle_a.h" // a includes b, which includes a // error: include cycle detected: 'tests/negative/include_cycle_a.h'
#endif without #if
- Cause: A stray
#endifencountered without an active conditional block. - Trigger (
tests/negative/stray_endif.cpp):#endif // error: #endif without #if
Unterminated #if/#ifdef/#ifndef block at EOF
- Cause: Reaching end of file while a conditional compilation block remains unclosed.
- Trigger (
tests/negative/unterminated_if.cpp):#ifdef MISSING // error: Unterminated #if/#ifdef/#ifndef block at EOF
Resource & Safety Diagnostics (ErrorKind::Internal)
Nesting depth exceeds the supported limit
- Cause: An input program exceeds calibrated recursion depth limits (e.g. >1,600 nested parentheses or >1,599 nested
ifblocks) to protect the WebAssembly shadow stack. - Trigger (
tests/negative/nesting_depth_limit.cpp):// 1601 nested parentheses: (((...(((1)))...))) // error: Nesting depth exceeds the supported limit
Compiler Architecture
cppbolt is organized as a single Cargo package that provides a core compiler library (cppbolt), a native CLI (cppbolt-cli), a corpus test runner (cppbolt-testrunner), and a code metrics scanner (cppbolt-census).
The core library is entirely OS-agnostic and dependency-free: it performs no direct filesystem I/O, spawns no child processes, and requires no multi-threading runtime. Host environments supply include files through the IncludeProvider trait.
The release WebAssembly compiler module built for wasm32-unknown-unknown is currently 842,723 bytes raw and 244,401 bytes with gzip -9 -n.
1. High-Level Pipeline
The compiler follows a direct, five-stage pipeline:
+-----------------------------------------------------------------+
| C++ Source Text (&str, filename) |
+-----------------------------------------------------------------+
│
â–¼
+-----------------------------------------------------------------+
| 1. Preprocessor (src/preprocessor/) |
| - Macro expansion (hide sets, ##, #) & rescan |
| - Conditional evaluation (#if, #elif, #ifdef) |
| - IncludeProvider resolution & cycle detection |
| - Origin line mapping for header diagnostics |
+-----------------------------------------------------------------+
│
â–¼ PreprocessedSource
+-----------------------------------------------------------------+
| 2. Lexer (src/lexer/) |
| - Tokenization into TokenKind variants |
| - Exact source span tracking (Span: FileId, line, col, len) |
+-----------------------------------------------------------------+
│
â–¼ Vec<Token>
+-----------------------------------------------------------------+
| 3. Parser & AST (src/parser/, src/ast/) |
| - Feature pre-scan (src/parser/unsupported.rs) |
| - Recursive descent + Pratt expression parser |
| - Nesting depth budgeting (src/depth.rs) |
+-----------------------------------------------------------------+
│
â–¼ ast::Program
+-----------------------------------------------------------------+
| 4. Semantic Analysis (src/sema/) |
| - Scopes, symbol registration, using/typedef aliases |
| - Inheritance & vtable slot calculation |
| - Record layout computation (alignment & padding) |
| - Overload resolution ranking & type checking |
| - C++17 copy elision tagging |
+-----------------------------------------------------------------+
│
â–¼ ast::Program + sema::Sema
+-----------------------------------------------------------------+
| 5. WASM Code Generation (src/codegen/, src/wasm/) |
| - Direct AST-to-Wasm instruction lowering (NO IR) |
| - Function context, local frame allocation |
| - Fixed-width LEB128 frame-size patching |
| - RAII deterministic scope cleanup emission |
| - Binary section encoding (src/wasm/) |
+-----------------------------------------------------------------+
│
â–¼
+-----------------------------------------------------------------+
| Emitted WebAssembly Binary (Vec<u8>) |
+-----------------------------------------------------------------+
2. Direct AST-to-Wasm Lowering (No IR, No Optimizer)
A fundamental architectural decision in cppbolt is the complete absence of an Intermediate Representation (IR) and an optimization pass (see ADR 0002).
- Traditional Compilers: Typically translate
AST -> High-Level IR -> Low-Level IR (SSA / 3-Address Code) -> Machine Instructions, running dozens of optimization passes in between. cppboltArchitecture: Lowers the annotated Abstract Syntax Tree (ast::Program) and semantic state (sema::Sema) directly into WebAssembly instruction bytes.- Why No IR?
- Keeps the compiler's own compiled WebAssembly payload extremely small (~244 KB gzipped).
- Maximizes compilation throughput in browser Web Workers.
- Eliminates the memory allocation overhead of building and tearing down intermediate CFG or SSA graphs.
- Why No Optimizer?
- Prioritizes compiler correctness, determinism, and transparent mapping to WebAssembly primitives.
- Generating unoptimized code is an intentional scope boundary for v1 (documented in
known-limitations.md).
3. Compiler Pipeline Stages
| Stage | Path | Purpose | Input Type | Output Type | Key Data Structures | Invariants |
|---|---|---|---|---|---|---|
| Preprocessor | src/preprocessor/ | Macro expansion, include resolution, #if evaluation | &str, &dyn IncludeProvider | PreprocessedSource | MacroDef, HideSet, LineOrigin | Include recursion capped at 128; macro recursion terminates via hide sets; origins accurately track header lines |
| Lexer | src/lexer/ | Tokenizes preprocessed text into lexemes | &str | Vec<Token> | Token, TokenKind, Span, FileId | Every token carries an exact non-fabricated source span |
| Parser & AST | src/parser/, src/ast/ | Parses grammar into typed AST | Vec<Token> | ast::Program | Program, Decl, Stmt, Expr, Type | Validates supported features first; nesting depth bounded per construct via depth.rs |
| Semantic Analysis | src/sema/ | Scope analysis, type checking, vtables, layouts, overloads | &mut ast::Program | sema::Sema | Sema, StructDef, FunctionSig, RecordLayout | Mutates AST with resolved types; rejects multiple/virtual inheritance; catches ambiguous overloads |
| WASM Codegen | src/codegen/, src/wasm/ | Lowers AST to WebAssembly binary sections | &ast::Program, &sema::Sema | Vec<u8> | Codegen<'a>, FunctionContext, WasmModule | Fixed-width LEB128 frame patches; stack allocation traps on guard page entry; RAII cleanups on all exits |
| Runtime | src/runtime/, stdlib/ | In-module allocator, printf runtime, embedded headers | N/A (Emitted or included) | Wasm opcodes | memory_layout, allocator, format | ILP32 32-bit addresses; stack top at 0x810000; heap grows upward; slot 0 traps |
4. Binary and Library Targets
The repository contains a single unified Cargo package defined in Cargo.toml:
| Target | Entry File | Responsibility |
|---|---|---|
cppbolt (lib) | src/lib.rs | Core compiler pipeline, public Rust API, and C ABI entry points (src/ffi.rs) |
cppbolt-cli (bin) | src/main.rs | Native command-line interface with filesystem include support and 64 MiB execution thread |
cppbolt-testrunner (bin) | src/bin/cppbolt-testrunner.rs | Corpus test runner, variant verification, native oracle execution, and golden baseline check |
cppbolt-census (bin) | src/bin/cppbolt-census.rs | Code quality scanner enforcing function line count (<80 lines) and parameter limits (<5 params) |
Preprocessor Internals
The preprocessor is the entry stage of the compilation pipeline, implemented in src/preprocessor/. It transforms raw C++ source text into an expanded translation unit while preserving accurate line origin metadata.
1. Role in the Pipeline
- Backslash-Newline Splicing: Splices lines ending in
\with subsequent lines prior to tokenization. - Include Directive Processing: Resolves
#include <...>and#include "..."using anIncludeProviderabstraction without performing direct filesystem operations in the core library. - Macro Expansion & Undefinition: Expands object-like and function-like
#definemacros using standard painted-blue hide sets, macro argument expansion, raw token concatenation (##), stringification (#), and recursive replacement rescanning. Removes macro definitions on#undef. - Conditional Compilation: Parses and evaluates constant integer expressions in
#if,#elif,#ifdef,#ifndef,#else, and#endifblocks, includingdefined(NAME)queries. - Origin Line Tracking: Records structured line origin mappings (
PreprocessedSource) so subsequent compiler errors from included headers accurately report the header file path and original line number.
2. Key Data Structures
Preprocessor: The main preprocessor driver holding macro symbol tables, active condition stacks, include depth counters, and the currentIncludeProvider.PreprocessedSource: Output struct holding the expanded source string (.text) and the line origin metadata table (.origins: Vec<LineOrigin>).LineOrigin: Represents the origin of a preprocessed line, mapping each output line back to its original file and line number.MacroDef: Stores macro definitions:#![allow(unused)] fn main() { pub enum MacroDef { Object(Vec<Token>), Function { params: Vec<String>, body: Vec<Token> }, } }HideSet: Set of macro names currently disabled during expansion to prevent infinite mutual recursion (standard C++ "painted-blue" algorithm).
3. Error Handling
The preprocessor returns Result<PreprocessedSource, CompileError>:
- Include Cycles: Detected by tracking active file paths on the include stack; returns
include cycle detected: '<path>'(tests/negative/include_cycle.cpp). - Include Depth Exhaustion: Enforces a strict limit of 128 nested include files to prevent call stack overflow.
- Unclosed Conditionals: Unterminated
#ifblocks at EOF triggerUnterminated #if/#ifdef/#ifndef block at EOF. - Stray Conditionals:
#endif without #ifor#else without #if. - Diagnostic Directives:
#error <msg>immediately stops preprocessing with an error diagnostic;#warning <msg>emits a diagnostic to stderr without halting compilation.
4. Real Input & Output Example
Taken from tests/c_basics/preprocessor/macro_rescan.cpp and tests/c_basics/preprocessor/preprocessor_macros.cpp:
Input C++ Source
#define STRINGIFY(x) #x
#define CONCAT(a, b) a ## b
#define VALUE 42
#define GET_VAL() VALUE
int CONCAT(test_, var) = GET_VAL();
const char* name = STRINGIFY(CONCAT(hello, world));
Preprocessed Output
int test_var = 42;
const char* name = "CONCAT(hello, world)";
The preprocessed text is emitted alongside line origin tables so any subsequent syntax or semantic error within test_var points to line 7 of the source file.
Lexer Internals
The lexer scans preprocessed C++ source text into a linear stream of strongly typed tokens, implemented in src/lexer/.
1. Role in the Pipeline
- Tokenization: Classifies source text into keywords, identifiers, numeric literals (decimal, hexadecimal, binary
0b, octal, floating-point with optional single-quote digit separators), string literals, character literals, and punctuation/operators. - Escape Decoding: Decodes standard C escape sequences in string and character literals (
\n,\t,\r,\\,\',\",\0, and hexadecimal/octal escapes) viasrc/lexer/literals.rs. - Span Attachment: Measures the exact line, column, and character length of every token, producing a
Spanused throughout the parser and semantic analyzer for accurate error reporting.
2. Key Data Structures
Lexer: Scans the input text slice maintaining current byte offset, line number, and column.Token: The fundamental token unit emitted by the lexer:#![allow(unused)] fn main() { pub struct Token { pub kind: TokenKind, pub line: usize, pub col: usize, pub length: usize, } }TokenKind: Enumeration of all accepted lexical symbols:- Literals:
IntLiteral(i64, String),FloatLiteral(f64, String),CharLiteral(char),StringLiteral(String),BoolLiteral(bool) - Identifiers:
Ident(String) - Keywords:
KwInt,KwReturn,KwClass,KwVirtual,KwIf,KwWhile,KwConst, etc. - Operators & Punctuators:
Plus,Minus,Star,Slash,Percent,AmpAmp,PipePipe,Arrow,ColonColon,LBrace,RBrace, etc. - Control:
Eof
- Literals:
Span: Represents source location ranges (file: FileId,line: usize,col: usize,len: usize).
3. Error Handling
The lexer produces CompileError with ErrorKind::Lexical:
- Unterminated String or Character Literals: Emits error if newline or EOF is encountered before the closing quote.
- Invalid Number Formats: Rejects malformed numeric literals (e.g. invalid exponent syntax, invalid binary or octal digits, or misplaced digit separators).
- Unrecognized Characters: Emits an error pointing to the unknown character.
4. Real Input & Token Output Example
Taken from tests/c_basics/types/arithmetic.cpp:
Input C++ Source
int main() {
int a = 10 + 20;
return a;
}
Resulting Token Stream
#![allow(unused)] fn main() { Token { kind: TokenKind::KwInt, line: 1, col: 1, length: 3 } Token { kind: TokenKind::Ident("main".into()), line: 1, col: 5, length: 4 } Token { kind: TokenKind::LParen, line: 1, col: 9, length: 1 } Token { kind: TokenKind::RParen, line: 1, col: 10, length: 1 } Token { kind: TokenKind::LBrace, line: 1, col: 12, length: 1 } Token { kind: TokenKind::KwInt, line: 2, col: 5, length: 3 } Token { kind: TokenKind::Ident("a".into()), line: 2, col: 9, length: 1 } Token { kind: TokenKind::Assign, line: 2, col: 11, length: 1 } Token { kind: TokenKind::IntLiteral(10, "10".into()), line: 2, col: 13, length: 2 } Token { kind: TokenKind::Plus, line: 2, col: 16, length: 1 } Token { kind: TokenKind::IntLiteral(20, "20".into()), line: 2, col: 18, length: 2 } Token { kind: TokenKind::Semi, line: 2, col: 20, length: 1 } Token { kind: TokenKind::KwReturn, line: 3, col: 5, length: 6 } Token { kind: TokenKind::Ident("a".into()), line: 3, col: 12, length: 1 } Token { kind: TokenKind::Semi, line: 3, col: 13, length: 1 } Token { kind: TokenKind::RBrace, line: 4, col: 1, length: 1 } Token { kind: TokenKind::Eof, line: 4, col: 2, length: 0 } }
Parser & AST Internals
The parser transforms the flat token stream into a structured Abstract Syntax Tree (ast::Program), implemented in src/parser/ and defined in src/ast/.
1. Role in the Pipeline
- Unsupported Feature Fast-Reject: Before executing the full grammar parser,
Parser::validate_supported_features(src/parser/unsupported.rs) scans the token stream to detect explicitly unsupported C++ constructs (e.g.operator(),operator->,operator*(),postfix ++/--, namespace aliases,constref method overloads) and returns clean diagnostics. - Grammar Parsing (Recursive Descent): Parses top-level declarations (functions, structs, classes, globals, enums, type aliases) and statements (
if,while,for,switch,return, compound blocks). Recognizes out-of-class constructor and destructor definitions across nested namespace and class qualifiers without requiring return type specifiers. - Expression Parsing (Pratt Parser): Parses expressions using precedence climbing / Pratt parsing (
src/parser/expr.rs), handling complex operator hierarchies, ternary conditionals, array subscripts, member access, casts, and object constructions. - Per-Construct Recursion Budgeting: To prevent thread stack exhaustion when compiling deep programs in WebAssembly engines, the parser tracks recursion limits via
DepthCountersacross expressions, parentheses, call arguments, statements, and blocks.
2. Key Data Structures
Parser: Holds the token slice, current token index, and activeDepthCounters.ast::Program: Root AST node containing a list of top-level declarations:#![allow(unused)] fn main() { pub struct Program { pub decls: Vec<Decl>, } }ast::Decl: Top-level declaration node:Function(FunctionDecl): Free functions, member methods, constructors, destructors.GlobalVar { name, ty, init, line, col }: Global and namespace-level variables.StructDecl { name, is_class, base_classes, fields, methods, constructors, destructor, ... }: Class and struct definitions.EnumDecl { name, variants }: Enumerations.TypeAlias { name, target_type }:typedefandusing Alias = Type.NamespaceDecl { name, decls }: Namespace scopes.
ast::Stmt: Statement nodes:Expr(Expr),Block(Vec<Stmt>),If { cond, then_branch, else_branch },While,DoWhile,For,RangeFor { decl, range, body },Switch,Return(Option<Expr>),Break,Continue,VarDecl.
ast::Expr: Expression tree nodes:IntLiteral,FloatLiteral,StringLiteral,BoolLiteral,Identifier,BinaryOp,UnaryOp,Call,MemberCall,MemberAccess,ArraySubscript,New,Delete,Ternary,Assign,Cast,Lambda.InitializerList { elements, pos }: Bare braced expression lists{ a, b, c }.TypedInitList { ty, elements, pos }: Explicitly-typed temporary and direct-list expressionsType{ a, b, c }.- Every
Exprvariant carries anExprPos(line,col) recording the position of its leading token.
ast::types::Type: Representation of types:Int,LongLong,Short,Char,Float,Double,Bool,Void,Nullptr(decltype(nullptr)).Pointer(Box<Type>, bool /* is_const */),Reference(Box<Type>),Array(Box<Type>, usize).Struct(String),Enum(String),FunctionPointer { ret, params }.
3. Error Handling & Invariants
- Syntax Errors: Emitted via
CompileError::new(..., line, col).with_kind(ErrorKind::Syntax)when an expected token (e.g. semicolon, identifier, closing delimiter) is missing. - Nesting Depth Guards: Bounded by
src/depth.rs:- Expressions: max 4,000
- Parentheses: max 1,600
- Compound blocks: max 1,600
- Statements: max 1,600
If any depth counter exceeds its budget, parsing terminates immediately with
ErrorKind::Internal("Nesting depth exceeds the supported limit").
- Unsupported Feature Rejection: Rejects constructs before partial AST creation to prevent invalid states in subsequent compiler passes.
4. Real Input & AST Example
Taken from tests/c_basics/types/arithmetic.cpp:
Input C++ Source
int main() {
int a = 10 + 20;
return a;
}
Resulting AST Representation
#![allow(unused)] fn main() { Program { decls: vec![ Decl::Function(FunctionDecl { name: "main".into(), return_type: Type::Int, params: vec![], body: Some(Stmt { kind: StmtKind::Compound(vec![ Stmt { kind: StmtKind::VarDecl { name: "a".into(), ty: Type::Int, init: Some(Expr::Binary { op: BinaryOp::Add, left: Box::new(Expr::IntLiteral(10, Type::Int)), right: Box::new(Expr::IntLiteral(20, Type::Int)), pos: ExprPos { line: 2, col: 13 }, }), }, line: 2, col: 5, }, Stmt { kind: StmtKind::Return(Some(Expr::Identifier( "a".into(), ExprPos { line: 3, col: 12 }, ))), line: 3, col: 5, }, ]), line: 1, col: 12, }), is_virtual: false, is_pure_virtual: false, is_const: false, is_static: false, ctor_initializers: vec![], line: 1, col: 1, }), ], } }
Semantic Analysis Internals
The semantic analysis stage validates the Abstract Syntax Tree, computes record memory layouts, resolves symbols, checks type conversions, and scores function overloads, implemented in src/sema/.
1. Role in the Pipeline
- Symbol Registration & Scoping: Registers struct definitions, function signatures, global variables, and type aliases across lexical and namespace scopes (
src/sema/analyze.rs). - Record Layout & Alignment: Computes struct field offsets, overall sizes, and alignment padding according to the 32-bit ILP32 target model (
src/sema/layout.rs). - Vtable Layout & Hierarchy Analysis: Analyzes single and linear inheritance hierarchies, computes base subobject offsets, identifies pure virtual methods (
= 0), and assigns vtable function pointer slots in base-prefix order. - Implicit Method Synthesis: Synthesizes default constructors, copy constructors, copy assignment operators, and destructors for structs that do not define them explicitly (
src/sema/synthesize.rs). Copy operations recursively copy base class subobjects before member fields. Dynamically instantiated template classes also synthesize cleanup methods and copy operations when member fields require non-trivial lifecycles. - Overload Resolution Ranking: Implements standard overload ranking rules to pick the best function or constructor candidate and detect ambiguous calls (
src/sema/overload.rs). Differentiates same-arity template candidates by parameter type signatures and binds out-of-class definitions using exact parameter types and constness. - Type Inference, Conversions & Qualification: Annotates expression nodes with resolved concrete types, validates assignments, arithmetic conversions, multi-level pointer
constqualification rules, null pointer constant conversions (0,0u,nullptr), and C++17 copy elision rules. - AST Desugaring: Lowers high-level syntax nodes (such as range-based
forloops insrc/sema/stmt_range_for.rs) into standard scoped primitives, synthesizing pointer and iterator bounds while preserving RAII and loop control flow. - Closure Synthesis & Lambda Desugaring: Synthesizes unique anonymous closure structs (
__lambda_N) for C++17 lambda expressions (src/sema/expr_lambda.rs), generating fields for captures, constructor initialization,operator()call signatures, return type deduction, and static__invoketrampoline methods for implicit conversion to C function pointers. - Recursive
operator->Resolution: Resolves chained member-arrow accesses recursively until a raw pointer is reached, enforcing cycle detection and receiverconstpropagation (src/sema/expr_member.rs). - List Initialization: Resolves braced initializers (
src/sema/init.rs) across direct-list, copy-list, and value-initialization contexts. Enforces C++17 narrowing rules ([dcl.init.list] p7) andexplicitconstructor rejection in copy contexts, and handles aggregate base subobjects and brace elision.
2. Key Data Structures
Sema: Central semantic state containing:structs: HashMap<String, StructLayout>: Layouts, field offsets, vtable slots, and pure virtual tracking for all defined records.functions: HashMap<String, Vec<FunctionSig>>: All registered functions and method overloads.globals: HashMap<String, Type>: Global and namespace variable types.type_aliases: HashMap<String, Type>:typedefandusingtype maps.
StructLayout:#![allow(unused)] fn main() { pub struct StructLayout { pub is_class: bool, pub has_user_declared_constructors: bool, pub base_classes: Vec<String>, pub base_offsets: Vec<(String, usize)>, pub fields: Vec<StructField>, pub size: usize, pub align: usize, pub has_vtable: bool, pub vtable_slots: Vec<String>, pub private_members: HashSet<String>, pub is_abstract: bool, pub pure_virtuals: HashSet<String>, } }FunctionSig: Signature metadata (parameters, return type,is_const,is_virtual, default arguments).OverloadCandidate: Candidate function paired with argument conversion ranks.
3. Overload Resolution Ranking
When a call expression matches multiple candidates, src/sema/overload.rs scores each parameter conversion against the provided arguments using five rank tiers:
- Exact Match: Argument type equals parameter type (or identical enum/primitive).
- Reference Binding: Binding an lvalue to
T&orconst T&. - Promotion: Numeric promotion (
char/short->int,float->double). - Standard Conversion: Derived-to-base pointer conversion, array decay, pointer qualification conversion (
T*->const T*), null pointer constant conversion (0/nullptr-> pointer /bool), pointer-to-void, or numeric widening/narrowing. - User-Defined Conversion: Implicit single-argument converting constructor.
Ambiguity Detection
If two candidates have identical rank scores across all arguments, overload resolution halts with:
file.cpp:line:col: error: call of overloaded 'func(...)' is ambiguous
Verified by tests/c_basics/functions/overload_resolution.cpp and tests/negative/ambiguous_overload_call.cpp.
4. Record Layout & Vtable Example
Given a polymorphic class hierarchy:
struct Base {
int id;
virtual void run();
};
struct Derived : public Base {
double value;
void run() override;
virtual void extra();
};
The semantic analyzer produces the following StructLayout for Derived:
size: 16 bytes (ILP32: 4 bytesvptrat offset 0, 4 bytesidat offset 4, 8 bytesvalueat offset 8).align: 8 bytes (due todoublealignment requirement).has_vtable:truevtable_slots:["Derived::run", "Derived::extra"](derivedrunreplacesBase::runat slot 0;extrais appended at slot 1).base_offsets:[("Base", 0)]
WebAssembly Code Generation Internals
The code generation stage lowers the typed AST and semantic symbol table directly into WebAssembly binary sections, implemented across src/codegen/ and encoded by src/wasm/.
1. Role in the Pipeline
- Direct AST-to-Wasm Lowering: As established in ADR 0002 and Architecture,
cppboltdoes not translate AST nodes into an intermediate representation (IR) or execute bytecode optimization passes. It walks the typed AST and writes WebAssembly opcodes directly into byte buffers. - Function Frame Management: Allocates checked local stack slots, emits prologue stack overflow checks, and coordinates RAII destructor calls before every function return point (
src/codegen/function_context.rs). - Fixed-Width LEB128 Patching: Because the final stack frame size is only known after traversing the entire function body, prologues and epilogues emit 5-byte fixed-width LEB128 placeholders that are patched in place after body lowering.
- Binary Section Assembly: Packs types, imports, functions, tables, memory, globals, exports, elements, code bodies, and data segments into the standard WebAssembly binary module format (
src/wasm/module.rs). - List Initialization Lowering: Emits zero-fill stores for empty braces (
{}), lowers aggregate field and base subobject stores (src/codegen/stmt_var_init.rs), allocates frame slots for typed temporaries (Type{args...}.member()), and generates loops for dynamic array initialization (new T[n]{...}) insrc/codegen/expr_new_array.rsto value-initialize trailing elements.
2. Key Data Structures
Codegen<'a>: The top-level code generator state:sema: &'a Sema: Reference to verified semantic layouts and symbols.module: WasmModule: The in-memory WebAssembly module.string_table: HashMap<String, u32>: Interned string literal offsets.next_data_offset: u32: Allocation pointer for static data starting atDATA_BASE.func_indices: HashMap<String, u32>: Function indices for direct calls.vtable_offsets: HashMap<String, u32>: Vtable offsets in static linear memory.
FunctionContext: Per-function lowering state:fp_local: u32: Local variable holding the frame pointer.scratch1..2, scratch_i64, scratch_f64: Reserved Wasm locals for temporary operand staging.var_offsets: HashMap<String, u32>: Byte offsets from the frame pointer for each local variable.frame_size: i32: Computed total frame size.frame_size_patches: Vec<usize>: Byte offsets in the code buffer requiring 5-byte LEB128 frame size patching.scope_objects: Vec<Vec<(String, Type, u32)>>: Active objects requiring RAII destruction at block/function exits.
WasmModule: In-memory representation of WebAssembly sections before serialization.
3. Function Frame Protocol & LEB128 Patching
Each generated function follows a strict stack frame protocol:
Call Entry
│
â–¼
[1. Prologue]
- Read global stack pointer (global 0)
- Check: new_sp < GUARD_PAGE_END (64 KiB)? -> unreachable trap
- Decrement global stack pointer by frame_size
- Set local fp = new_sp
│
â–¼
[2. Function Body]
- Lower expressions & statements
- Allocate locals at [fp + offset]
- Register RAII destructors
│
â–¼
[3. Epilogue / Return]
- Run active destructors in reverse construction order
- Restore global stack pointer = fp + frame_size
- Return value
The 5-Byte LEB128 Placeholder Technique
Because the total frame size is only known after all nested blocks and temporaries in a function are compiled, the prologue and epilogue emit a fixed 5-byte LEB128 placeholder:
#![allow(unused)] fn main() { // In FunctionContext::emit_frame_size code.push(OP_I32_CONST); // 0x41 self.frame_size_patches.push(code.len()); code.extend_from_slice(&[0x80, 0x80, 0x80, 0x80, 0x00]); }
Once the body completes and self.frame_size is calculated, FunctionContext::patch_frame_sizes overwrites the 5 bytes with the true value padded with continuation bits. This guarantees that code offsets and branch destinations never shift during compilation.
4. Real Input & WebAssembly Opcode Output Example
Taken from tests/c_basics/types/arithmetic.cpp:
Input C++ Source
int main() {
int a = 10 + 20;
return a;
}
Emitted WebAssembly Instructions (WAT equivalent)
(func $main (result i32)
(local $fp i32)
(local $scratch1 i32)
(local $scratch2 i32)
(local $scratch_i64 i64)
(local $scratch_f64 f64)
(local $scratch_f32 f32)
(local $ret_i32 i32)
(local $ret_i64 i64)
(local $ret_f64 f64)
;; Prologue: allocate 16-byte aligned frame
global.get 0
i32.const 16
i32.sub
local.tee $fp
global.set 0
;; Guard check
local.get $fp
i32.const 65536
i32.lt_u
if
unreachable
end
;; int a = 10 + 20; (store at [fp + 0])
local.get $fp
i32.const 0
i32.add
i32.const 10
i32.const 20
i32.add
i32.store offset=0
;; return a; (load from [fp + 0])
local.get $fp
i32.const 0
i32.add
i32.load offset=0
local.set $ret_i32
;; Epilogue: restore stack pointer
global.get 0
i32.const 16
i32.add
global.set 0
local.get $ret_i32
return
)
Runtime & Embedded Standard Library
cppbolt provides a self-contained runtime environment comprising an ILP32 linear memory layout, an in-module WebAssembly free-list allocator, an embedded printf formatting engine, and compile-time embedded standard library headers.
1. Linear Memory Model & ABI
Programs compiled with cppbolt use 32-bit ILP32 addresses. Linear memory is partitioned into four distinct segments defined in src/runtime/memory_layout.rs:
+--------------------------+ 0x000000 (0)
| Reserved Guard Region | 64 KiB
| - Address 0 is Null ptr |
+--------------------------+ 0x010000 (65,536)
| |
| Downward-Growing Stack | 8 MiB
| |
| |
+--------------------------+ 0x810000 (8,454,144 = STACK_TOP / DATA_BASE)
| Static Data Segment |
| - String literals | Variable size
| - Global/static vars |
| - Vtables |
+--------------------------+
| |
| Upward-Growing Heap | Expands via memory.grow
| (Managed by allocator) | up to max_memory_mb (default 256 MiB)
| |
+--------------------------+
Safety & Invariants
- Stack Overflow Guard: Frame allocation in function prologues verifies that the decremented stack pointer remains strictly greater than
65,536(GUARD_PAGE_END). Entering the guard region causes an immediate WebAssemblyunreachabletrap (tests/runtime_trap/stack_overflow.cpp). - Function Table Slot 0: Index 0 of the WebAssembly indirect call table (
funcref) is explicitly reserved for a trapping stub. Indirect function calls through a null function pointer trap deterministically (tests/runtime_trap/null_function_pointer.cpp). - Virtual Method Dispatch: Objects of polymorphic classes place their vtable pointer (
vptr) at byte offset 0. Table slots strictly follow base-prefix order.
2. In-Module Free-List Memory Allocator
Dynamic memory allocation is managed by an in-module free-list allocator implemented directly in WebAssembly instructions (src/runtime/allocator.rs):
- Block Structure: Free and allocated memory chunks are prefixed with an 8-byte aligned header storing the block size and allocation flag.
- Coalescing: Adjacent free blocks are automatically merged on
freeto prevent heap fragmentation. - Heap Growth: When existing free blocks cannot satisfy a request, the allocator issues
memory.growto expand linear memory up to the configured limit. - Allocation Failure Semantics:
malloc(size)returns0(null pointer) on allocation failure.operator newchecks the result ofmallocand triggers anunreachableWebAssembly trap if null is returned (since exceptions likestd::bad_allocare excluded in v1 per ADR 0005).
Verified by tests/memory_model/allocator_correctness.cpp, tests/memory_model/free_list_reuse.cpp, and tests/runtime_trap/malloc_beyond_max.cpp.
3. Embedded Standard Library (stdlib/)
Headers under stdlib/ are embedded into the compiler binary at compile time using Rust's include_str! macro (ADR 0004).
stdlib/
├── iostream # std::cout, std::cin, streams
├── string # std::string implementation
├── cstdio, stdio.h # printf, sprintf, snprintf, puts
├── cstdlib, stdlib.h # malloc, free, abs, exit
├── cmath # sqrt, sin, cos, abs, etc.
├── iomanip # std::setprecision, std::fixed, std::setw
├── new # operator new, placement new
├── utility # std::move, std::forward
├── initializer_list # std::initializer_list
└── cstddef # size_t, ptrdiff_t, NULL
Present vs. Supported Split
- Supported Headers: Streams (
<iostream>), string manipulation (<string>), C stdio (<cstdio>), math (<cmath>), manipulators (<iomanip>), and memory allocation (<new>,<cstdlib>). Fully covered by corpus tests. - Partial Headers:
<vector>,<map>,<set>, and<algorithm>. These files are present instdlib/to compile specific mini-STL test fixtures, but full template container functionality is outside the v1 subset (tracked as Planned).
Independent Native Header Verification
All embedded headers are compiled and verified against host g++ and clang++ using -nostdinc++ and AddressSanitizer/UndefinedBehaviorSanitizer via tools/verify_mini_stl_gxx.py.
Standard I/O Contract
- Shared Lookahead: C stdio (
scanf,getchar) and C++ streams (cin >>) share an internal character lookahead buffer to allow interleaved input. - Limitations: Mixing C and C++ input after
std::ios::sync_with_stdio(false)is outside the supported contract. Streamunget()is not supported.
Testing System & Verification Gates
The compiler test suite is managed by cppbolt-testrunner. Tests are discovered automatically by directory structure, categorized by feature area, and validated against multiple independent gates.
The expected passing baseline is defined in tests/PASSING.txt. To run the test suite and verify current counts:
cargo build --release
cargo run --bin cppbolt-testrunner -- target/release/cppbolt-cli
1. Test Execution & Exit Codes
The test runner discovers tests by directory:
- Positive Tests: Contain
<name>.cppand<name>.expectedfiles (with optional<name>.inputproviding stdin). - Runtime Trap Tests: Assert that execution produces a non-zero WebAssembly
unreachabletrap (tests/runtime_trap/). - Negative Tests: Contain
<name>.cppand<name>.expected_errfiles, asserting exact diagnostic output (tests/negative/).
The command syntax is:
cppbolt-testrunner [compiler|ref] [group]
The first argument selects the compiler binary (or ref to run host g++). The optional second argument filters execution to a single feature group.
The Passing Manifest (tests/PASSING.txt)
A failing test that is absent from tests/PASSING.txt is treated as an expected failure (e.g. planned template and STL container tests): the runner executes it, reports the failure under Expected failures, but does not cause the process to exit with an error.
The runner exits 0 if and only if:
- Every test listed in
tests/PASSING.txtpasses. - Every passing test is listed in
tests/PASSING.txt. - Every discovered test executes without unhandled errors.
- The structural variant gate passes.
- The independent test manifest matches.
Unexpected test failures or manifest discrepancies exit 1.
2. Test Categories
Main Tests (tests/<feature>/)
Contains hand-written and generated tests covering supported C++ features. Every supported language or library construct must have a corresponding positive test in this directory.
Anti-Overfitting Variants (tests/<feature>_variant/)
Every main test has a counterpart in a matching _variant group. Variants guard against the compiler overfitting to specific test inputs. The runner compares each same-named test pair and enforces:
- Fail: Sources that are byte-identical or differ only in comments/whitespace fail the gate.
- Warn: Sources that differ only in identifier names or numeric literals emit a warning.
- Pass: Sources that differ structurally (e.g. using helper functions, loops, or scoped blocks) pass cleanly.
Cleanroom Independent Tests (tests/independent/)
Tests in tests/independent/ are authored without inspecting compiler implementation code, main tests, or variant tests. Authors work strictly from the C++17 ISO specification and public compiler documentation.
- Expected outputs are generated with
g++ -std=c++17 -O0and verified withclang++ -std=c++17 -O0. - All files in the directory are cryptographically pinned in
tests/independent/MANIFEST.sha256. The test runner fails if any file changes or disappears without owner approval.
Generated Differential Tests
Large parameter matrices (such as lvalue_matrix.cpp generated by tools/generate_lvalue_matrix.py and printf_table.cpp generated by tools/generate_printf_table.py) are generated mechanically to exhaustively verify operator and formatting combinations against native compilers.
3. Golden Baseline & Native Oracle Gates
Golden Hashes (tests/golden.txt)
Before any behavior-preserving refactor, verify the golden baseline:
cargo run --release --bin cppbolt-testrunner -- --check-golden tests/golden.txt target/release/cppbolt-cli
tests/golden.txt records the SHA-256 hashes of both the emitted .wasm binary and standard output for every compiling positive test. A pure refactor must leave every hash unchanged. Regenerate intentionally with --golden only when compiler behavior or binary encoding changes deliberately.
Native Oracle Gate (--oracle)
The test runner can replay every positive test through native host compilers:
cargo run --release --bin cppbolt-testrunner -- --oracle
python3 tools/verify_mini_stl_gxx.py
The oracle compiles each test with g++ -std=c++17 -O0 and clang++ -std=c++17 -O0 and asserts that native stdout matches the checked-in .expected files. This gate also runs in CI via the integration test tests/oracle_gate.rs.
4. Size & Complexity Census (cppbolt-census)
To maintain long-term codebase health, src/bin/cppbolt-census.rs scans Rust source files and reports function lengths, parameter counts, and file sizes:
cargo run --release --bin cppbolt-census
Verified by tests/census_fixture.rs, the project strictly enforces:
- No function may exceed 80 lines.
- No function may accept more than 5 parameters (excluding
self). - No source file in
src/may exceed 500 lines.
ADR Template
Status
[Proposed | Accepted | Accepted (reconstructed) | Rejected | Superseded]
Context
What is the problem or architectural requirement being addressed? What constraints exist in the compiler, runtime, or WebAssembly target?
Options Considered
- Option 1: Description, pros, and cons.
- Option 2: Description, pros, and cons.
Decision
What was chosen and why?
Consequences
Positive
- Benefits and guarantees provided by this decision.
Negative / Trade-offs
- Limitations, complexities, or deferred features resulting from this choice.
ADR 0001: Implement Custom Subset Compiler Instead of Embedding LLVM
Status
Accepted (reconstructed)
Context
Running C++ code in the browser typically relies on Emscripten and LLVM/Clang (docs/decisions/001-own-compiler.md, commit d23afed). However, shipping LLVM or Clang inside a browser environment (or compiling Clang itself to WebAssembly) results in tens of megabytes of binary payload, high memory usage, long startup latency, and unpredictable cold starts (docs/decisions/001-own-compiler.md, commit d23afed, README.md, commit 57df2cf). Furthermore, for educational playgrounds or targeted web workloads, the full complexity of C++ is not required .
Options Considered
- Option 1: Embed Clang/LLVM compiled to WebAssembly
- Pros: Full C++17/C++20 standards compliance out of the box; mature optimization pipeline.
- Cons: Enormous shipped wasm size (>30 MB), multi-second browser initialization latency, heavy memory overhead.
- Option 2: Implement a custom, dependency-free C++ compiler in Rust
- Pros: Predictable shipped binary size (~840 KB raw, ~244 KB gzipped), instant browser initialization, zero runtime dependencies, explicit and tightly controlled language subset boundaries.
- Cons: Must author the preprocessor, lexer, parser, semantic analyzer, and code generator from scratch; standard library and complex features must be built incrementally.
Decision
Implement a custom C++ subset compiler directly in Rust targeting WebAssembly (docs/decisions/001-own-compiler.md, commit d23afed, README.md, commit 57df2cf). The compiler accepts an explicit, well-defined subset of C++17 and emits WebAssembly binary format directly.
Consequences
Positive
- Shipped WebAssembly size is kept under 1 MB uncompressed and ~245 KB gzipped (
README.md, commit57df2cf). - Zero external runtime or compiler dependencies; fast startup in browsers and Web Workers (
docs/decisions/001-own-compiler.md, commitd23afed,Cargo.toml). - The accepted language subset is explicit, deterministic, and tailored for browser execution (
docs/decisions/001-own-compiler.md, commitd23afed).
Negative / Trade-offs
- Features outside the subset (e.g. templates, full standard library algorithms, exceptions, RTTI) must be rejected or implemented by hand (
docs/decisions/005-no-exceptions-rtti.md, commitd23afed,src/parser/unsupported.rs).
ADR 0002: Direct WebAssembly Emission Without Intermediate Representation or Optimization
Status
Accepted (reconstructed)
Context
Compilers traditionally lower an AST to one or more Intermediate Representations (e.g. SSA form, 3-address code, control flow graphs) before executing optimization passes and target code generation . This adds substantial conceptual and runtime complexity, increases memory consumption during compilation, and requires additional data structures that enlarge the compiled wasm binary of the compiler itself (docs/decisions/002-direct-wasm.md, commit d23afed).
Options Considered
- Option 1: Multi-stage pipeline with an explicit IR and bytecode optimizer
- Pros: Potential for constant folding, dead code elimination, and register/local allocation optimizations.
- Cons: Higher compiler binary size, more complex codebase, increased memory usage during compilation, slower compilation throughput in browser workers.
- Option 2: Direct syntax-directed emission from typed AST to WebAssembly binary sections
- Pros: Highly transparent lowering; minimal memory overhead; rapid compilation speed; simple direct mapping between C++ language semantics and WebAssembly instructions.
- Cons: Generated code is unoptimized; scratch locals and stack allocations are conservative.
Decision
The backend writes WebAssembly binary sections directly from the typed AST and semantic symbol table without an intermediate representation or optimization pipeline (docs/decisions/002-direct-wasm.md, commit d23afed, src/codegen/mod.rs, src/wasm/module.rs). Generated-code optimization is explicitly out of scope for v1 (docs/decisions/002-direct-wasm.md, commit d23afed).
Consequences
Positive
- The compiler remains small, fast, and dependency-free (
docs/decisions/002-direct-wasm.md, commitd23afed,Cargo.toml). - Target ABI and generated instructions directly reflect source semantics, making debugging and verification predictable (
docs/decisions/002-direct-wasm.md, commitd23afed,tests/golden/). - Clean separation between semantic analysis and opcode emission (
src/sema/mod.rs,src/codegen/mod.rs,src/lib.rs#L34-L52).
Negative / Trade-offs
- Generated WebAssembly bytecode contains unoptimized instruction sequences (e.g. redundant local loads/stores) because optimization is out of scope (
docs/decisions/002-direct-wasm.md, commitd23afed). - Frame allocation must be conservatively managed via fixed offsets and LEB128 patch points (
src/codegen/function_context.rs#L24-L50).
ADR 0003: 32-bit ILP32 Data Model
Status
Accepted (reconstructed)
Context
WebAssembly standard MVP specifies 32-bit linear memory indexing (wasm32) using i32 addresses (docs/decisions/003-ilp32.md, commit d23afed, src/wasm/opcodes.rs). C++ data models vary across platforms (LP64 on modern 64-bit Unix, LLP64 on Windows, ILP32 on 32-bit platforms) . To target WebAssembly 32-bit linear memory, a consistent data representation is required for integers, pointers, and memory offsets (docs/decisions/003-ilp32.md, commit d23afed).
Options Considered
- Option 1: 64-bit memory model with WebAssembly Memory64 (
wasm64)- Pros: Aligns with 64-bit desktop conventions; larger address space.
- Cons: Memory64 is not universally supported in browser runtimes; 64-bit pointers waste cache and linear memory in small WebAssembly workloads.
- Option 2: 32-bit ILP32 model (
wasm32)- Pros: Direct mapping to WebAssembly MVP standard (
i32.load,i32.store, 32-bit address space); stable structure alignments across 32-bit targets; compact data structures. - Cons: Maximum linear memory is bounded by 4 GiB (and configured by default to 256 MiB in
cppbolt).
- Pros: Direct mapping to WebAssembly MVP standard (
Decision
Target the ILP32 data model (docs/decisions/003-ilp32.md, commit d23afed, src/ast/types.rs#L40-L70):
sizeof(int) == 4sizeof(long) == 4sizeof(long long) == 8sizeof(pointer) == 4(represented as Wasmi32addresses)sizeof(float) == 4(Wasmf32)sizeof(double) == 8(Wasmf64)
Consequences
Positive
- Memory layouts and struct alignments remain stable across native test harnesses and browser hosts (
docs/decisions/003-ilp32.md, commitd23afed,src/runtime/memory_layout.rs,tests/oracle_gate.rs). - Direct alignment with WebAssembly linear memory instructions without pointer widening or narrowing (
src/codegen/expr.rs#L120-L160,src/wasm/opcodes.rs).
Negative / Trade-offs
- Programs assuming 64-bit
longtypes must uselong longfor 64-bit integer arithmetic (src/ast/types.rs,tests/c_basics/types/long_long.cpp).
ADR 0004: Compile-Time Embedded Standard Library Headers
Status
Accepted (reconstructed)
Context
When running inside a browser Web Worker or sandboxed environment, the compiler cannot perform asynchronous or synchronous host filesystem reads to load #include <iostream> or other standard headers (docs/decisions/004-embedded-stdlib.md, commit d23afed, web/playground-worker.js). Packaging a virtual filesystem (VFS) or separate header bundle complicates asset loading and deployment (docs/decisions/004-embedded-stdlib.md, commit d23afed).
Options Considered
- Option 1: Virtual Filesystem (VFS) or external bundle loaded over HTTP
- Pros: Headers can be updated without recompiling the compiler binary.
- Cons: Requires network round-trips, asynchronous preloading in web workers, and complex filesystem shims.
- Option 2: Embed headers as static string slices via
include_str!- Pros: Zero filesystem or network dependencies at runtime; headers are always available instantly; compiler binary is completely self-contained.
- Cons: Headers must be authored and maintained directly within the repository; slightly increases compiler binary size.
Decision
Embed the mini standard library headers (stdlib/) directly into the compiler binary using Rust's include_str! macro (docs/decisions/004-embedded-stdlib.md, commit d23afed, src/preprocessor/mod.rs#L34-L50). The compiler's IncludeProvider resolves standard include directives (e.g. <iostream>, <string>, <cstdio>) from these embedded string slices without touching the filesystem.
Consequences
Positive
- The mini standard library is available without host filesystem access in browser Web Workers (
docs/decisions/004-embedded-stdlib.md, commitd23afed,src/preprocessor/mod.rs#L34-L50). - Headers are independently checked against real C++ compilers (
g++andclang++with-nostdinc++) viatools/verify_mini_stl_gxx.pyandtests/oracle_gate.rs(docs/decisions/004-embedded-stdlib.md, commitd23afed,tests/oracle_gate.rs). - Deterministic header compilation behavior across both native CLI and WebAssembly-hosted builds (
src/lib.rs#L40-L48).
Negative / Trade-offs
- Standard library modifications require recompiling the compiler crate (
src/preprocessor/mod.rs). - Only headers written to comply with the supported C++17 subset can be included (
src/parser/unsupported.rs).
ADR 0005: Exclusion of Exceptions and RTTI from v1 Subset
Status
Accepted (reconstructed)
Context
Exceptions and RTTI are excluded from v1 (docs/decisions/005-no-exceptions-rtti.md, commit d23afed). Unsupported constructs produce diagnostics rather than silently lowering to a different runtime model (docs/decisions/005-no-exceptions-rtti.md, commit d23afed, src/parser/unsupported.rs). Full C++ runtime features like exception handling (try/catch/throw) and Run-Time Type Information (typeid, dynamic_cast) require substantial runtime machinery (stack unwinding tables, personality functions, type descriptor tables) that are omitted to keep the direct lowering model simple and deterministic (docs/decisions/005-no-exceptions-rtti.md, src/codegen/mod.rs).
Options Considered
- Option 1: Implement WebAssembly exception handling proposal or Emscripten-style setjmp/longjmp emulation
- Pros: Supports idiomatic C++ error handling.
- Cons: Immensely complicates the direct WebAssembly code generator; requires complex unwinding frames; emulated unwinding severely degrades performance.
- Option 2: Exclude exceptions and RTTI from the v1 compiler subset
- Pros: Code generator remains clean, simple, and deterministic; RAII cleanups are strictly local and stack-based; zero unwinding runtime overhead.
- Cons: C++ programs relying on
try/catchor polymorphic dynamic casting are not supported in v1.
Decision
Exclude exceptions and RTTI from the v1 compiler subset (docs/decisions/005-no-exceptions-rtti.md, commit d23afed). Unsupported constructs produce explicit compile-time diagnostics (ErrorKind::Unsupported or ErrorKind::Syntax) rather than silently lowering to incomplete or hazardous runtime behaviors (src/parser/unsupported.rs, src/diagnostics/mod.rs).
Consequences
Positive
- Unsupported constructs produce clear compile-time diagnostics rather than silently lowering to a different runtime model (
docs/decisions/005-no-exceptions-rtti.md, commitd23afed,tests/negative/). - Destructor invocations and RAII cleanups follow straightforward lexical scope exit rules (
src/codegen/stmt.rs#L180-L240,tests/classes_oop/memory_raii/class_raii.cpp). - Vtables contain only function pointers for virtual method dispatch, without type descriptor pointers (
src/codegen/vtables.rs#L20-L45).
Negative / Trade-offs
- Code using
throw,try,catch,typeid, ordynamic_castfails compilation with explicit diagnostics (src/diagnostics/mod.rs,src/parser/expr.rs). - Allocator failures in
newtrap deterministically instead of throwingstd::bad_alloc(src/runtime/allocator.rs#L85-L102).
ADR 0006: WebAssembly Linear Memory Layout and Stack Safety
Status
Accepted (reconstructed)
Context
WebAssembly provides a single contiguous linear memory array per instance (src/runtime/memory_layout.rs, commit 0b21628). The compiler must layout static data, the execution stack for locals whose addresses are taken, and dynamic heap allocations while preventing silent memory corruption between stack and heap (src/runtime/memory_layout.rs, commit 0b21628, tests/wasm_stack_safety.rs).
Options Considered
- Option 1: Upward-growing stack, downward heap
- Pros: Simple stack increment.
- Cons: Stack overflow grows into data/heap areas, causing silent memory corruption unless bounds checks are added to every allocation.
- Option 2: 64 KiB guard region, downward-growing stack, upward-growing heap
- Pros: Follows standard ABI conventions; stack overflows hit a reserved zero-page guard region; clear separation between stack and heap.
- Cons: Stack pointer must be initialized to a high address and decremented per call frame.
Decision
Establish the linear memory layout defined in src/runtime/memory_layout.rs (commit 0b21628):
- Guard Page: Address
0to65,535(64 KiB). Reserved region. Address0serves as the null pointer representation. - Stack Region: Address
65,536(64 KiB) to8,454,143(8 MiB + 64 KiB). Grows downwards. The global stack pointer is initialized toSTACK_TOP(8,454,144). - Static Data Region: Begins at
DATA_BASE(STACK_TOP). Holds string literals, static variables, and vtables. - Heap Region: Begins immediately after static data and grows upwards toward the maximum configured memory limit (default 256 MiB).
In function prologues, stack frame allocations check that the new stack pointer does not decrement below GUARD_PAGE_END (65,536). If it does, the program immediately triggers a WebAssembly unreachable trap (src/codegen/function_context.rs#L50-L75, tests/wasm_stack_safety.rs).
Consequences
Positive
- Stack overflow cannot silently corrupt static data or the heap; it traps deterministically before entering the guard page (
src/runtime/memory_layout.rs#L40-L65,tests/wasm_stack_safety.rs). - Address zero serves as the null pointer representation and is never returned by valid allocations (
src/runtime/memory_layout.rs#L12-L20). - Fixed constants ensure reproducibility between native test harnesses and browser hosts (
src/runtime/memory_layout.rs#L8-L30,tests/browser_flow.rs).
Negative / Trade-offs
- Maximum stack depth is fixed at 8 MiB per WebAssembly instance (
src/runtime/memory_layout.rs#L22). - Prologue emissions require explicit stack bounds checks against
GUARD_PAGE_END(src/codegen/function_context.rs#L50-L75).
ADR 0007: WebAssembly Free-List Memory Allocator Implementation
Status
Accepted (reconstructed)
Context
Programs in C++ frequently allocate and free dynamic memory via malloc/free and new/delete (tests/classes_oop/memory_raii/new_delete.cpp, tests/memory_model/free_list_reuse.cpp). Because cppbolt is dependency-free, it cannot link dlmalloc or external C standard libraries, but must emit its own allocator directly into the generated WebAssembly module (src/runtime/allocator.rs, commit 0b21628).
Options Considered
- Option 1: Bump allocator without
freesupport- Pros: Extremely simple to generate; very small code footprint.
- Cons: Long-running or loop allocations in the browser playground leak memory monotonically and quickly exhaust linear memory.
- Option 2: In-module free-list allocator with block coalescing
- Pros: Reuses freed memory blocks; supports standard dynamic lifecycle; self-contained in generated bytecode.
- Cons: Requires generating allocator logic (
malloc,free) into every WebAssembly binary that performs dynamic allocation.
Decision
Emit a dedicated free-list allocator directly in WebAssembly bytecode (src/runtime/allocator.rs, commit 0b21628):
- Block headers track size, free status, and link pointers with 8-byte alignment (
src/runtime/allocator.rs#L30-L75). - Freed blocks are returned to a free list and adjacent free blocks are coalesced (
src/runtime/allocator.rs#L140-L195). - When existing blocks cannot satisfy an allocation,
memory.growis invoked to expand the WebAssembly linear memory up to the configured limit (src/runtime/allocator.rs#L210-L245). - Allocation Failure Semantics:
malloc(size)returns0(null pointer) on allocation failure (src/runtime/allocator.rs#L85-L100).operator newchecks the result ofmallocand executes anunreachableWebAssembly trap if allocation fails (as exceptions such asstd::bad_allocare out of scope per ADR 0005) (src/runtime/allocator.rs#L102-L115).
Consequences
Positive
- Dynamic allocations can be safely reused, validated by
tests/memory_model/free_list_reuse.cpp(src/runtime/allocator.rs, commit0b21628). - Clean separation between C-style null-return semantics for
mallocand C++ trapping semantics fornew(src/runtime/allocator.rs#L85-L115). - Zero external runtime library dependencies (
src/runtime/allocator.rs,Cargo.toml).
Negative / Trade-offs
- Adds a small fixed bytecode footprint (~2–3 KB) to generated modules that use dynamic memory (
src/runtime/allocator.rs). - Free-list search is first-fit, which is suitable for subset workloads but not optimized for high-concurrency fragmentation .
ADR 0008: String Representation and Interning
Status
Accepted (reconstructed)
Context
Strings in C++ exist both as static string literals (const char*) and dynamically resizable objects (std::string) (tests/c_basics/io_printf/printf_conversions.cpp, tests/mini_stl/string_basic.cpp). The compiler requires a deterministic layout for string literals in the WebAssembly data section and a memory-safe structure for std::string in the embedded standard library (src/codegen/strings.rs, stdlib/string, commit 0b21628).
Options Considered
- Option 1: Duplicate string literals per occurrence
- Pros: Simple code generation.
- Cons: Bloats WebAssembly data section and linear memory footprint.
- Option 2: Intern string literals into the static data section and represent
std::stringvia dynamic heap buffers- Pros: Minimizes data section footprint; deduplicates identical literals; provides standard null-terminated
const char*interoperability. - Cons: Requires interning bookkeeping during compilation.
- Pros: Minimizes data section footprint; deduplicates identical literals; provides standard null-terminated
Decision
- String Literals (
const char*):- String literals are deduplicated and interned into
Codegen::string_table(src/codegen/strings.rs, commit0b21628). - Interned strings are placed consecutively with trailing null terminators (
\0) into the WebAssembly data section starting atmemory_layout::DATA_BASE(src/runtime/memory_layout.rs#L25-L35).
- String literals are deduplicated and interned into
- Standard String (
std::string):- Defined in
stdlib/stringusing three fields:char* _data,size_t _len, andsize_t _cap(commit0b21628). - Dynamic expansions use
_ensure_capwith geometric buffer doubling backed bymallocandfree(stdlib/string#L30-L50). - Small String Optimization (SSO) is omitted in v1 in favor of predictable buffer layouts (
stdlib/string).
- Defined in
Consequences
Positive
- Identical string literals share single data section addresses via interning (
src/codegen/strings.rs#L30-L55,tests/c_basics/preprocessor/preprocessor.cpp). std::string::c_str()provides a zero-overhead pointer compatible with C standard library functions likeprintfandputs(stdlib/string#L45-L60,tests/mini_stl/string_basic.cpp).- Destructors cleanly release heap memory on scope exit via RAII (
stdlib/string#L62-L75,tests/classes_oop/memory_raii/class_raii.cpp).
Negative / Trade-offs
- Without Small String Optimization (SSO), small strings always perform a heap allocation (
stdlib/string#L30-L45).
ADR 0009: WebAssembly C ABI and JavaScript Host Interoperability
Status
Accepted (reconstructed)
Context
When running the compiler inside Node.js or a browser Web Worker, JavaScript must pass source strings into the WebAssembly instance and retrieve compiled WebAssembly binaries or error messages without memory leaks (src/ffi.rs, web/playground-worker.js, tests/ffi_lifecycle.js). Furthermore, compiled C++ programs need an execution environment for basic I/O and process exit (web/host.js, commit d23afed, tests/browser_flow.rs).
Options Considered
- Option 1: Use
wasm-bindgenwith complex JavaScript glue generation- Pros: Automatic conversion between JS and Rust types.
- Cons: Introduces large external dependencies, complex build tools, and generated wrapper scripts that complicate standalone embedding.
- Option 2: Low-level C ABI with manual buffer management and a standardized JavaScript shim (
web/host.js)- Pros: Dependency-free; zero code bloat; explicit ownership semantics; portable across Node.js, Web Workers, and native embedders.
- Cons: Requires disciplined pair-calling of allocation and deallocation APIs.
Decision
- Compiler C ABI Entry Points (
src/ffi.rs):alloc_buffer(size): Allocates an uninitialized input buffer for source code (src/ffi.rs#L30-L40).dealloc_buffer(ptr, size): Frees an allocated input buffer if compilation is aborted (src/ffi.rs#L42-L50).compile_wasm(ptr, len): Compiles UTF-8 source. Returns pointer to compiled.wasmbytes, or0on error (src/ffi.rs#L52-L80).get_result_len(): Returns length of the successful result buffer (src/ffi.rs#L82-L88).free_result(ptr, len): Explicitly frees the boxed slice containing the compiled WebAssembly binary (src/ffi.rs#L90-L98).get_error_ptr()/get_error_len(): Returns the static diagnostic buffer (owned by the compiler; never freed by host) (src/ffi.rs#L100-L115).
- JavaScript Host Runtime (
web/host.js):- Implements standard
envimports for generated programs:print_char,print_int,print_double,read_char,exit(web/host.js#L15-L60). - Formats floating point values and integer conversions in JavaScript matching the compiler's runtime contract (
web/host.js#L62-L80).
- Implements standard
Consequences
Positive
- Zero external build or runtime dependencies; the compiler is a pure
cdylib(Cargo.toml,src/ffi.rs). - Repeated compilations keep linear memory bounded without leaks, as proven by
tests/ffi_lifecycle.js(tests/ffi_lifecycle.rs). - Identical
host.jsshim runs in both Node.js test harnesses and browser Web Workers (web/host.js,tests/browser_flow.rs).
Negative / Trade-offs
- Hosts must adhere strictly to the allocation lifecycle: buffers must be released with their matching deallocation calls (
src/ffi.rs#L30-L70,tests/ffi_lifecycle.js).
ADR 0010: Template Instantiation Model and Single-Phase Lookup
Status
Accepted
Context
Compiling templated standard library headers like <vector>, <utility>, and <algorithm> requires supporting class and function templates with type parameters (stdlib/vector, stdlib/utility, stdlib/algorithm, src/ast/template.rs, src/sema/template_registry.rs). Standard C++ specifies two-phase lookup: non-dependent names bind at definition time, and dependent names bind at instantiation time. Implementing two-phase lookup in cppbolt would require duplicate symbol tables and separate typing passes for uninstantiated ASTs before concrete types exist. In addition, real-world headers require out-of-class member method definitions, member function templates within both class templates and plain structs, dependent member type aliases, and multi-arity overload sets.
Options Considered
- Option 1: Two-phase lookup conforming to ISO C++.
- Rejects invalid non-dependent symbols in uninstantiated templates early.
- Requires separate symbol resolution and typing passes over uninstantiated AST structures, adding substantial parser and semantic analysis complexity.
- Option 2: Single-phase lookup at instantiation time.
- Fits the existing compiler pipeline; the parser stores uninstantiated template declarations (
TemplateDecl), and semantic analysis checks and lowers AST nodes when concrete types are supplied. - Malformed non-dependent code in uninstantiated templates remains undetected unless instantiated.
- Fits the existing compiler pipeline; the parser stores uninstantiated template declarations (
Decision
-
Single-phase name lookup (
src/ast/template.rs,src/sema/template_registry.rs). We store template declarations directly inProgramasTemplateDeclnodes without early name binding (src/ast/template.rs). We validate and resolve types only when a concrete specialization is requested (src/sema/template_registry.rs). -
Canonical specialization identity (
src/sema/template_registry.rs). We name each specialization using its template name and canonical argument types (such asHolder<int>orstd::vector<int>). We cache instantiated struct and function declarations inSemaso identical specializations reuse the same generated definitions. -
Lazy member function instantiation (
src/sema/template_registry.rs,src/sema/expr_ops.rs). We lay out fields and register member function signatures when instantiating a class template. We defer member function body analysis and emission until called or referenced, permitting template arguments that do not support unused member methods. Non-virtual destructors instantiate eagerly alongside virtual methods so synthesized cleanup logic can drop values on scope exit. -
Out-of-class template method resolution (
src/sema/template_registry.rs). When the parser encounterstemplate <typename T> void Class<T>::method(), we look up the primary class template and attach the method AST directly to its declaration before any instantiation occurs. -
Member function templates (
src/sema/constructor.rs,src/sema/template_registry.rs). Member methods declaring their own template parameters (such aspair(U1&&, U2&&)orDefaultLess::operator()<T>) bypass eager class method registration and register intomember_templates. We deduce their arguments and synthesize concrete methods during call resolution. For non-template structs, instantiated methods synthesize into dedicated struct declarations so codegen compiles them with concrete types. -
Instantiated dependent type aliases (
src/sema/template_registry.rs). When instantiating a class template that declares member type aliases (such asusing iterator = T*), we substitute the template arguments into the target type and register the qualified alias (such asstd::vector<int>::iteratorresolving toint*). -
Multi-arity function template candidate sets (
src/sema/template_registry.rs,src/sema/template_func.rs). Function template registration maintains a candidate vector for each identifier. Overload resolution filters candidates by parameter count and default arguments before argument deduction, allowing overloads likesort(first, last)andsort(first, last, comp)to coexist. -
Recursion depth budget (
src/sema/template_registry.rs). We cap recursion depth at 500 levels, rejecting recursive template expansions with an explicit diagnostic before exhausting the WebAssembly call stack. -
Nested class instantiation in class templates (
src/sema/template_nested.rs). When instantiating a class template containing member struct declarations (such asAVLTree<T>::Node), we capture nested AST declarations inTemplateClassand clone them with outer type arguments substituted. Each nested struct receives its qualified canonical name (such asstd::detail::AVLTree<int, int>::Node), computes its record layout, and registers synthesized lifecycle hooks and member methods. -
Dependent type specifiers and typename disambiguation (
src/parser/types.rs,src/sema/template_nested.rs). We parse thetypenameprefix before qualified type specifiers in template and function declarations (typename T::iterator). Semantic analysis resolves dependent names through qualified alias tables and registered nested types, rejecting unqualifiedtypenamespecifiers and non-type dependent names with distinct diagnostics. -
Eager constructor dependency analysis (
src/sema/constructor.rs,src/sema/stmt.rs). When analyzing an instantiated constructor, semantic analysis inspects member initializer expressions and base class subobjects after parameter bindings enter scope. Dependent default constructors for member subobjects and array elements instantiate immediately, populating member layouts and destructor cleanups before WebAssembly code emission.
Consequences
Positive
- Reuses existing AST substitution and struct layout routines without separate uninstantiated typing passes (
src/sema/template_subst.rs,tests/templates_operators/template_class_basic.cpp). - Unused member methods are neither analyzed nor emitted to WebAssembly, keeping output binaries compact (
tests/templates_operators/template_lazy_member.cpp). - Standard containers like
std::vectorandstd::paircompile and execute with full RAII semantics, dynamic growth, and range-based for loops (tests/mini_stl/vector_basic.cpp,tests/mini_stl/iterators_range_for.cpp). - Associative containers
std::map,std::set, and their backingdetail::AVLTreecompile cleanly with full nested node structures, balance rotations, and iterator traversals (tests/mini_stl/map_basic.cpp,tests/mini_stl/set_basic.cpp,tests/mini_stl/differential_fuzz.cpp). - Sorting algorithms with default or custom comparators resolve correctly across multiple arities (
tests/mini_stl/algorithm_sort.cpp,tests/mini_stl/sort_comparator.cpp).
Negative / Trade-offs
- Ill-formed non-dependent expressions in uninstantiated template bodies do not trigger compile errors unless an instantiation requests them.
Contributing Guide
Thank you for contributing to cppbolt! This document outlines development workflows, code standards, how to add a language feature end-to-end, and the required pull request checklist.
1. Development Environment & Build
Building the project requires a standard Rust toolchain (2024 edition).
# Build the native CLI compiler (target/release/cppbolt-cli)
cargo build --release
# Run Rust unit and integration tests
cargo test --all-targets
# Run formatting and linting gates
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
2. Running Tests & the Compiler
Running the Corpus Runner
All C++ test suites (positive tests, negative diagnostics, and runtime traps) are managed by cppbolt-testrunner:
# Run all discovered test groups
cargo run --bin cppbolt-testrunner -- target/release/cppbolt-cli
# Run a specific feature group (e.g. c_basics)
cargo run --bin cppbolt-testrunner -- target/release/cppbolt-cli c_basics
For full details on the test categories (variants, independent tests, golden hashes, native oracle gates, and census metrics), see the dedicated Testing System Guide.
Compiling and Running a Sample
Compile a C++ source file using the native CLI:
# Compile C++ source to a WebAssembly binary
cargo run --release --bin cppbolt-cli -- path/to/sample.cpp -o output.wasm
You can run the resulting .wasm binary in Node.js using the test runner shim:
node tests/test_wasm_compiler.js output.wasm
Or test it in the browser playground:
cargo build --release --target wasm32-unknown-unknown --lib
tools/build_site.sh
python3 -m http.server --directory site 8000
3. Adding a New Language Feature End-to-End
When adding a new C++ language feature or expression form to cppbolt, modifications typically flow through each stage of the compiler pipeline:
- Tokens (
src/lexer/):- If the feature introduces new keywords or operators, add variants to
src/lexer/token.rsand update lexer scanning insrc/lexer/mod.rs.
- If the feature introduces new keywords or operators, add variants to
- Abstract Syntax Tree (
src/ast/):- Define corresponding AST nodes in
src/ast/expr.rs,src/ast/stmt.rs,src/ast/decl.rs, orsrc/ast/types.rs. Ensure nodes record line and column positions for diagnostic accuracy.
- Define corresponding AST nodes in
- Parser (
src/parser/):- Add parsing logic in
src/parser/expr.rs,src/parser/stmt.rs, orsrc/parser/decl.rs. - If lifting a restriction on a previously unsupported feature, update
src/parser/unsupported.rs. - Track nesting limits using
self.depth(src/depth.rs) to prevent stack overflow on pathological inputs.
- Add parsing logic in
- Semantic Analysis (
src/sema/):- Implement type checking, name lookup, and conversions in
src/sema/analyze.rs,src/sema/expr.rs, orsrc/sema/lookup.rs. - Synthesize necessary implicit copy/default constructors or record layouts.
- Implement type checking, name lookup, and conversions in
- Code Generation (
src/codegen/):- Lower the typed AST node to WebAssembly bytecodes in
src/codegen/expr.rs,src/codegen/stmt.rs, or dedicated helpers. - Respect stack frame conventions in
src/codegen/function_context.rsand memory layout rules insrc/runtime/memory_layout.rs.
- Lower the typed AST node to WebAssembly bytecodes in
- Corpus Tests (
tests/):- Add positive test cases (
.cppand.expected) intests/<feature_group>/. - Add corresponding anti-overfitting structural tests in
tests/<feature_group>_variant/. - If rejecting invalid syntax, add a negative test (
.cppand.expected_err) intests/negative/. - Update
tests/PASSING.txtand regenerate golden hashes if expected changes are approved:cargo run --release --bin cppbolt-testrunner -- --golden tests/golden.txt target/release/cppbolt-cli
- Add positive test cases (
4. Code Standards & Repository Rules
- No Compiler Logic Modifications Without Tests: Every supported behavior must have a positive corpus test. Every unsupported construct must produce a clean diagnostic, not a crash or panic.
- Panic-Free Policy: Never use
unwrap(),expect(), orpanic!()on code paths that can process user input. UseResult<T, CompileError>. - Function and File Size Limits: Enforced by
src/bin/cppbolt-census.rsand verified bytests/census_fixture.rs:- No function may exceed 80 lines.
- No function may take more than 5 parameters (excluding
self). - No file in
src/may exceed 500 lines.
- Module Tree Reachability: Every Rust source file under
src/must be wired into the crate module tree; verified bytests/module_tree.rs. - Preserve Documentation Integrity: Comments should document invariants, ABI decisions, and memory models, rather than narrating line-by-line changes.
5. Pull Request Checklist
Before submitting a pull request, ensure all of the following items are complete:
-
Tests Pass:
cargo test --all-targetsexits 0. -
Corpus Runner Passes:
cargo run --bin cppbolt-testrunner -- target/release/cppbolt-clireports 0 unexpected failures and 0 manifest violations. -
Code Quality Gates Pass:
cargo fmt --all -- --checkcargo clippy --all-targets -- -D warningscargo run --release --bin cppbolt-censusreports 0 violations.
- Feature Matrix Updated: New features or changed statuses are reflected in Supported C++ with links to test files.
-
Documentation Updated: Relevant mdBook chapters or doc comments (
///,//!) updated. -
ADR Created (if applicable): An Architecture Decision Record was added to
docs/adr/if an architectural, memory model, or ABI decision was made.
Development Log
Ongoing chronological record of major milestones, engineering refactors, and architectural evolutions in cppbolt.
2026-11-03: std::initializer_list Review Fixes
Fixed five findings in the initializer-list and brace-initialization support added earlier the same day.
Summary of Work
- Resolved non-template method parameter and return types in
register_struct_declarations(src/sema/analyze.rs), so a constructor such asX(std::initializer_list<int>)orX(std::vector<int>)matches an argument whose template type was already instantiated (c78aa25). - Ranked every initializer-list constructor instead of taking the first whose elements were convertible: exact conversions beat integral promotions, which beat other standard conversions. A narrowing candidate is skipped, and its diagnostic is reported only when no candidate is viable (
fb4a983). - Added empty initializer-list constructor fallback for value-initialization (
T x{}) when the class declares no default constructor (fb4a983). - Normalized extracted template element types to their registered struct names and resolved nested brace lists against the chosen element type, enabling
std::vector<std::vector<int>> v{{1, 2}, {3, 4}}(cadf45c). - Routed a namespace-scope brace initializer through the same list resolution as locals, and lowered a namespace-scope
std::initializer_list<T>by building its backing array inmain's frame and storing the pointer and length in the global object (366bad8).
Known Issue
Range-based for loops and structured bindings inside non-template method bodies are not analyzed: analyze_declarations type-checks a clone of the body and discards it, so the loop compiles to nothing. Writing the analyzed body back changes 137 golden hashes; corpus tests avoid method-body loops for now. Free functions and template method bodies are unaffected.
Verification Results
- Corpus: 428 tests on disk, 428 passed, 0 unexpected failures; all 118 negative tests passed.
- Golden baseline: all 310 entries match
tests/golden.txt, with only the two new tests added. - Census audit: 0 files over 500 lines, 0 functions over 80 lines, 0 functions with more than 5 parameters excluding self.
- Clippy: 0 warnings with
-D warningsacross all targets.
2026-11-03: std::initializer_list and Brace Initialization
Implemented std::initializer_list<T> and container brace initialization.
Summary of Work
- Added the vector count-and-value constructor so
vector<int>(3, 4)remains distinct fromvector<int>{3, 4}. - Implemented semantic initializer-list resolution with constructor preference,
autodeduction, homogeneous element checks, narrowing diagnostics, and support for parenthesized braced arguments. - Lowered each list to a frame-backed contiguous element array plus an eight-byte
{pointer, length}std::initializer_list<T>value. Temporary backing elements are destroyed at full-expression cleanup; local list elements are destroyed with their containing scope. - Added positive, variant, and negative corpus coverage for list arguments,
autodeduction, vector brace initialization, mismatched types, emptyauto, and narrowing.
Verification Results
- Corpus: 416 tests on disk, 416 passed, 0 unexpected failures; all 114 negative tests passed.
- New positive coverage includes
std::vector<std::string>brace initialization andvector({1, 2, 3, 4}).
2026-11-02: Template Method Defaults, Lifecycle Synthesis, and Out-of-Class Qualified Attachment
Resolved review findings in member template default argument mapping, same-arity template candidate deduplication, parameter shadowing, out-of-class qualified constructor parsing, and base subobject copying.
Summary of Work
- Corrected member template default argument indexing in
src/sema/constructor.rs, mapping default argument expressions across the full signature parameter slice including the leading implicit instance receiver. - Deduplicated function template candidates in
src/sema/template_registry.rsusing exact parameter types rather than arity alone, preserving coexisting same-arity overloads and updating bodies on forward declarations. - Aligned out-of-class template method attachment with parameter types and constness, preventing same-arity overloads from overwriting existing definitions.
- Extended constructor and destructor detection in
src/parser/template.rsto traverse multi-part namespace and class qualifier paths (Outer::Inner::Class::Class()). - Added in-place out-of-class non-template method attachment in
src/sema/analyze.rs, attaching method and constructor bodies to target struct declarations prior to layout and semantic checking. - Enforced member template parameter shadowing during outer class template substitution in
src/sema/template_subst.rs, preserving inner template parameters. - Refined functor dispatch in
src/sema/expr_call.rsto verify that member templates matchoperator()before rewriting instance calls. - Included base class initializers in synthesized copy constructors and prepended base assignments in copy assignment operators in
src/sema/synthesize.rs, with base subobject offset adjustments insrc/codegen/call_args.rsandsrc/codegen/lvalue.rs. - Added dynamic destructor and copy operation synthesis for instantiated template classes containing members with non-trivial lifecycles.
- Added 8 unit tests in
src/sema/tests.rscovering all resolved cases, and updated golden baseline hashes across 13 affected tests.
Verification Results
- Unit and integration tests: 394 corpus tests on disk, 390 passed, 4 expected failures, 0 unexpected failures. All 16 unit tests in
src/sema/tests.rsand 88 library unit tests passed. - Golden baseline: all 283 entries match
tests/golden.txt. - Census audit: 0 files over 500 lines, 0 functions over 80 lines, 0 functions with more than 5 parameters excluding self.
- Clippy: 0 warnings with
-D warningsacross all targets.
2026-11-02: Member Function Templates, Standard Vector, and Algorithm Overloads
Implemented member function templates, out-of-class template method definitions, dependent member type aliases, and multi-arity function template overloads. These additions unlock <vector>, <utility> (std::pair), and <algorithm> (std::sort, std::reverse, std::min, std::max).
Summary of Work
- Added parsing and registration for out-of-class template method definitions (
template <typename T> void Class<T>::method()) insrc/parser/decl.rsandsrc/sema/template_registry.rs. - Supported member function templates inside class templates and concrete structs in
src/parser/structs.rs,src/sema/constructor.rs, andsrc/sema/template_registry.rs. This enables templated constructors and call operators such aspair(U1&&, U2&&)and functor invocations. - Enabled dependent member type aliases on instantiated template classes (such as
std::vector<int>::iteratortoint*) insrc/parser/types.rsandsrc/sema/template_registry.rs. Range-based for loops over template containers now resolve iterators and dereferenced elements directly. - Expanded function template registration in
src/sema/template_registry.rsto track candidate lists rather than single declarations. Candidate filtering by arity and default arguments enables overloads such asstd::sort(first, last)andstd::sort(first, last, comp)to coexist. - Handled member template instantiations for plain structs (such as
DefaultLess::operator()<T>) by synthesizing concrete method declarations into the AST for WebAssembly codegen. - Added constructor conversion for by-value struct arguments in
src/codegen/call_args.rs, creating managed temporaries when passing arguments such as string literals to struct parameters. - Synthesized copy constructors and copy assignment operators for classes containing non-trivial subobjects in
src/sema/synthesize.rs, and updated placement new and member initializers insrc/codegen/expr_new.rsandsrc/codegen/function_lifecycle.rsto copy struct bytes when explicit copy constructors are absent. - Extracted
src/sema/template_func.rs,src/sema/synthesize.rs, andsrc/sema/expr_call_member.rsto maintain repository census compliance. - Added 5 variant tests:
tests/templates_operators_variant/template_class_methods_variant.cpp,tests/mini_stl_variant/pair_basic.cpp,tests/mini_stl_variant/vector_basic.cpp,tests/mini_stl_variant/iterators_range_for_variant.cpp,tests/mini_stl_variant/algorithm_sort_variant.cpp, andtests/mini_stl_variant/combined_integration_variant.cpp.
Verification Results
- Unit and integration tests: 394 corpus tests on disk, 390 passed, 4 expected failures (remaining associative container tests), 0 unexpected failures. All 107 negative tests passed.
- Host oracle gate: matched native compiler behavior on all expected files (82.2s).
- Census audit: 0 files over 500 lines, 0 functions over 80 lines, 0 functions with more than 5 parameters excluding self.
- Clippy: 0 warnings with
-D warningsacross all targets.
2026-11-01: Template Integration Bugfixes and Lifecycle Lowering
Resolved edge cases across template type resolution, field layout calculation, lazy operator lowering, RAII destructor lifecycles, and function argument deduction.
Summary of Work
- Made type resolution recursive across pointers, references, const qualifiers, and arrays in
src/sema/template_registry.rs, generating concrete struct layouts before field lookup. Added a cycle guard insrc/sema/lookup.rsto stop infinite alias looping. - Resolved template instance types in struct fields before computing struct layouts in
src/sema/analyze.rs, setting correct field offsets and total struct sizes. Resolved template types in non-template function parameters and return types during declaration registration and function analysis. - Lowered lazy template operator methods (
operator[], unary operators) upon match insrc/sema/expr_ops.rs. Eagerly instantiated non-virtual template class destructors insrc/sema/template_registry.rsso destructor synthesis registers cleanup calls on scope exit. - Filled default arguments on function template calls via
self.full_arguments(&sig, args)insrc/sema/expr_call.rs, providing all required stack values in WebAssembly bytecode. - Passed known class templates into explicit template argument list parsing in
src/sema/template_deduce.rsto support nested types likefunc<Inner<int>>(x). - Stripped top-level const during pointer argument deduction in
src/sema/template_deduce.rsto deduceTfromT* const. - Refined template name substitution in
src/sema/template_subst.rsto prevent accidental replacement across scope qualifiers. - Added regression tests for pointer access, const-reference binding, struct template fields, non-template functions with template arguments, overloaded subscript operators, destructor scope cleanups, function default arguments, nested explicit types, and const pointer deduction (
template_ptr_const_ref.cpp,template_field_and_func.cpp,template_ops_dtor.cpp,template_default_arg_deduce.cpp).
Verification Results
- Unit and integration tests: 381 corpus tests on disk, 364 passed, 17 expected failures, 0 unexpected failures. All 107 negative tests passed.
- Golden baseline: updated to 267 entries in
tests/golden.txt, all matchingtarget/debug/cppbolt-cli. - Census audit: 0 files over 500 lines, 0 functions over 80 lines, 0 functions with more than 5 parameters excluding self.
- Clippy: 0 warnings with
-D warningsacross all targets.
2026-11-01: Template Core Implementation
We implemented class and function templates with type parameters, single-phase name lookup, lazy member instantiation, and function template argument deduction.
Summary of Work
- Added template AST definitions in
src/ast/template.rs, includingTemplateDecl,TemplateParam, andType::TemplateInstance. - Implemented parsing for template declarations, template argument lists (
<T, U>), and template-ids insrc/parser/template.rs. Added diagnostics rejecting unsupported constructs: explicit specialization, explicit instantiation, template template parameters, variadic templates, and non-type template parameters. - Implemented AST substitution in
src/sema/template_subst.rs, replacing type parameters with concrete types across expressions, statements, types, and declarations. - Implemented template registry in
src/sema/template_registry.rsto lay out struct fields, assign canonical specialization names, cache instantiated declarations, and lower member methods lazily on call. - Implemented function template argument deduction in
src/sema/template_deduce.rs, deducing concrete types from call argument types, handling reference and const qualifiers, and checking type consistency across repeated parameters. - Integrated function templates into overload resolution in
src/sema/expr_call.rs. The compiler prefers exact matches, falls back to function templates when available, and respects defaulted arguments during candidate selection. - Added positive regression tests covering class templates, function templates, multi-parameter templates, reference deduction, and distinct instantiations (
template_class_basic,template_func_basic,template_multi_param,template_pair,template_func_multi,template_lazy_member,template_multi_instance,template_deduce_ref). - Added negative tests verifying rejection of template recursion exceeding the 500 depth limit and deduction conflicts (
template_instantiation_depth.cpp,template_deduction_conflict.cpp).
Verification Results
- Unit and integration tests: 377 corpus tests on disk, 360 passed, 17 expected failures, 0 unexpected failures. All 107 negative tests passed.
- Host oracle gate: matched native compiler behavior on all expected files (73.6s).
- Golden baseline: updated to 263 entries in
tests/golden.txt, all matchingtarget/debug/cppbolt-cli. - Census audit: 0 files over 500 lines, 0 functions over 80 lines, 0 functions with more than 5 parameters excluding self.
- Clippy: 0 warnings with
-D warningsacross all targets.
2026-11-01: Scalar Operations and Baseline Synchronization
The test suite, clippy, census audit, and golden manifest are green across all targets.
Summary of Work
- Synchronized golden baseline: reconciled 49 stale entries in
tests/golden.txtinitially, then added all newly passing tests, bringing the total to 255 verified entries. - Added
std::abs(long)andstd::abs(long long)overloads instdlib/cmath. Unsigned types now trigger ambiguity errors instead of silently promoting to float. - Implemented functional casts and value initialization (
T(),Type(val)) for scalar types and typedef aliases in the parser and sema. - Added scalar pseudo-destructor support (
p->~T(),x.~T(),arr[i].~T()). The compiler evaluates receiver expressions for side effects and treats the destructor call as a no-op. - Resolved struct destructor calls through typedef aliases (
sp->~ClassT()), checking that the alias resolves to the receiver class before calling the destructor. - Corrected
classes_oop/lambdas/lambda_unary_plusto compare pointer size againstsizeof(void*), allowing the test to pass both in 32-bit WebAssembly and under the 64-bit native oracle gate.
Verification Results
- Unit and integration tests: 370 corpus tests on disk, 351 passed, 19 expected failures, 0 unexpected failures, 0 manifest violations.
- Oracle gate: 544 matched, 2 runtime-trap tests skipped across g++ and clang++.
- Golden manifest: 255 entries recorded in
tests/golden.txt, all matchingtarget/release/cppbolt-cli. - Census audit: 119 files scanned, 0 files over 500 lines, 0 functions over 80 lines, 0 functions with more than 5 parameters excluding self.
- Clippy: 0 warnings with
-D warningsacross all targets.
2026-11-01: Baseline Snapshot and Scalar Prerequisite Probes
Context & Motivation
We reconciled test runner manifests and probed compiler behavior on typedef stand-in types representing scalar type parameters (typedef int T;).
Work Executed
- Golden manifest reconciliation:
- Synchronized
tests/golden.txtagainst commit 13051c384029eb2f4b05a4e958b8ca893234c23f. - Resolved 49 stale entries that differed due to previous qualification conversions, numeric literals, and brace-initialization updates.
- Re-verified clean golden output across all 247 recorded test entries with
cargo run --bin cppbolt-testrunner -- --check-golden tests/golden.txt target/release/cppbolt-cli.
- Synchronized
- Scalar prerequisite probes:
- Value-initialization
T a = T();: failed withUndefined function 'T'. The parser interpretsT()as a function call instead of a scalar functional cast. - Direct-initialization
T b(5);: passed. - Size evaluation and allocation
sizeof(T)and(T*)malloc(...): passed. - Scalar placement new
new (buf) T(7);: passed. - Scalar pseudo-destructor
p->~T(): failed withArrow member access on non-struct pointer. Member access rejects non-struct pointers and lacks pseudo-destructor grammar. - Class destructor call via typedef
sp->~ClassT(): failed withMethod '~ClassT' not found in class 'S'. Destructor lookup does not resolve typedef aliases to target class names.
- Value-initialization
Verification
- Synchronized golden manifest passes: 247 entries matched, 19 uncompilable tests skipped.
- Corpus test suite execution: 360 total tests on disk, 341 passed, 19 expected failures, 0 unexpected failures. All 94 negative tests passed.
2026-11-01: Type Qualification, Pointer Conversions, and String Literal Semantics
Context & Motivation
Work on this date addressed C++17 pointer conversion semantics, qualification conversions, and string literals:
- The compiler allowed arbitrary integers and variables to convert to pointers instead of restricting null pointer constants to literal zero and
nullptr. nullptrwas represented internally asvoid*, collapsingstd::nullptr_ttype identity and distorting overload ranking.- Multi-level pointer qualification conversions did not enforce C++17 rules, incorrectly allowing
int**to convert toconst int**. - String literals were typed as pointers rather than
const char[N], causing incorrectsizeofresults and permitting assignment tochar*. - Implicit conversion from
void*to typed pointers was accepted without an explicitstatic_cast.
Work Executed
nullptrtype identity:- Added
Type::Nullptr(mangledDn) fornullptr,std::nullptr_t, anddecltype(nullptr), with implicit conversions to pointers andbool. - Mapped
KwNullptrandnullptr_ttokens toType::Nullptracross lexer and parser.
- Added
- Pointer qualification and null pointer constants:
- Implemented null pointer constant validation in
src/sema/assign.rs, accepting only literal zeroes (0,0u) andnullptr. - Implemented
check_pointer_conversioninsrc/sema/assign.rsfor multi-level qualification conversions, requiringconston all preceding levels when introducingconstat an inner level. - Allowed object-pointer-to-
void*conversion; rejected implicitvoid*-to-typed-pointer conversions. - Updated overload resolution (
score_arguments) and constructor ranking (score_constructor_candidate) to score qualification conversions and null pointer constant conversions.
- Implemented null pointer constant validation in
- String literal semantics:
- Represented string literals as
Type::Array(Type::Char(const), len + 1)insrc/sema/expr.rsand codegen. - Preserved array typing through
sizeof(sizeof("hello") == 6) and decayed toconst char*for call arguments and pointer assignments. - Rejected assignments of string literals to mutable
char*.
- Represented string literals as
Verification
- Added positive tests in
tests/c_basics/types/pointer_conversions.cppfor null pointer constants,nullptr, multi-level qualification,void*, and string literals. - Added 7 negative diagnostic tests in
tests/negative/. - 69 library and binary tests pass (
cargo test --lib --bins). - 337 corpus tests pass without regressions (
cargo run --bin cppbolt-testrunner). - Census metrics and clippy pass with zero warnings.
2026-10-30: C++17 Numeric Literals, Macro Undef, and Raw Token Pasting
Context & Motivation
Work on this date expanded literal syntax and fixed macro token pasting:
- The lexer did not support binary literals (
0b1010), digit separators (1'000'000), or octal digit checks. - The preprocessor could not undefine macros (
#undef). - Macro token pasting (
##) expanded operand macros before pasting instead of concatenating raw argument tokens.
Work Executed
- Numeric literals:
- Parsed binary literals with
0band0Bprefixes. - Supported single-quote digit separators, rejecting consecutive quotes or quotes at literal boundaries.
- Rejected digits
8and9in octal literals.
- Parsed binary literals with
- Preprocessor directives and macro expansion:
- Added the
#undefdirective insrc/preprocessor/directive_line.rs. - Updated
src/preprocessor/macros.rsto paste raw tokens directly on##without prior expansion.
- Added the
Verification
- Added positive tests in
tests/c_basics/types/numeric_literals.cppandtests/c_basics/preprocessor/macro_token_paste_raw.cpp. - Added negative diagnostic tests in
tests/negative/for invalid binary digits, misplaced digit separators, and invalid octal digits. - Formatting and clippy clean.
2026-10-29: List Initialization, Array Decay, and Constructor Resolution
Context & Motivation
Work on this date addressed list initialization and several compiler edge cases exposed during testing:
- Braced initializer lists were previously limited to array declarations and struct assignments. Direct initialization (
Type x{42}), value initialization (Type x{}), C++17 aggregate subobjects, narrowing conversions, typed temporaries (Type{}.get()), and bracednewallocations were unsupported. - Multi-dimensional pointer dereferencing (
*(*(matrix + 1) + 2)) mistakenly emitted scalar loads on intermediate array addresses rather than decaying to row base pointers. - Namespace-scope global variables with parenthesized constructor initializers (
Item global_item(1);) conflicted with function declaration grammar. - User-defined conversion operators and pointer constructors were improperly matching copy constructor lookups during by-value argument passing and returns.
- Free
operator*overloads were asymmetric between rvalue reading and lvalue assignment (*it = 42;).
Work Executed
- List initialization:
- Added
Expr::TypedInitList { ty, elements, pos }to the AST and attached source positions toExpr::InitializerList. - Updated Pratt expression parsing to handle typed initializers and braced lists across variable declarations, member initializers, return statements, arguments, assignments, and
newexpressions. ExtractedParser::parse_brace_expr_listinsrc/parser/mod.rsto deduplicate grammar loops. - Implemented
resolve_list_initinsrc/sema/init.rsdriven byInitContext, allowing explicit constructors in direct-list contexts while rejecting them in copy-list contexts. - Enforced narrowing conversion checks per C++17 [dcl.init.list] p7 for floats, doubles, and integer types, with compile-time range evaluation for constants and negative literals.
- Added C++17 aggregate classification (
is_cxx17_aggregate) rejecting classes with user-declared constructors (layout.has_user_declared_constructors), private members, or virtual functions, while supporting public non-virtual base subobjects and flattened brace elision. - Extended code generation in
src/codegen/stmt_var_init.rsfor empty brace zeroing, added dynamic array allocation with trailing element value-initialization insrc/codegen/expr_new_array.rs, and allocated frame slots for typed temporaries.
- Added
- Pointer dereference and array decay:
- In
src/codegen/expr_unary.rs, updated dereference emission so that when the dereferenced element is an array (such asint (*)[3]), the base address stays on the stack instead of emitting a scalar load, allowing subsequent pointer arithmetic to compute proper offsets.
- In
- Global variable constructor disambiguation:
- In
src/parser/decl.rs, introducedparse_ambiguous_global_or_funcwith lookahead for function qualifiers and bodies, falling back to parenthesized constructor expressions for global variable definitions.
- In
- Copy constructor resolution:
- In
src/codegen/construct.rs, introducedfind_copy_constructorrequiring the parameter to be a reference, preventing conversion operators or pointer constructors from being picked as copy constructors.
- In
- Free
operator*lvalue assignment:- In
src/codegen/lvalue.rs, routed assignments through freeoperator*overloads returning references (T& operator*(Iter)).
- In
Verification
- Added positive tests in
tests/classes_oop/structs/for scalar initialization, class constructors, aggregates, brace elision, temporaries, and dynamicnew. - Added negative tests in
tests/negative/for narrowing conversions, explicit constructor rejection, and excess initializer counts. - Added regression tests for multi-dimensional pointer dereferencing, global constructor arguments, and conversion operator parameter passing.
- 51 library tests pass (
cargo test --lib). - 324 corpus tests pass without manifest violations (
cargo run --bin cppbolt-testrunner). - Formatting and clippy clean.
2026-10-28: Lambdas, Operator Overloading, and Language Conformance
Context & Motivation
Work on this date added core C++17 language constructs, expanded operator overloading, and implemented lambda expressions:
- Smart pointer, iterator, and functor abstractions required
operator*(), pointer member accessoperator->, function calloperator(), and postfixoperator++(int)/operator--(int). - Chained smart pointer delegation required recursive
operator->resolution with cycle detection. - C++17 lambdas (captures, return type deduction, direct call, and conversion to function pointers) were needed for modern functional patterns.
- Compiler compatibility audits revealed gaps in array parameter decay, constexpr global initializer reachability, delegating constructors, switch case constant expressions, scoped enums (
enum class), user conversion operators, ref-qualified member methods, structured bindings, and explicitthrowrejection diagnostics.
Work Executed
- Operator overloading:
- Enabled parsing and semantic resolution for unary dereference
operator*(), pointer member accessoperator->, functoroperator(), and postfixoperator++/--(distinguished via dummyintparameter). - In
src/sema/expr_member.rs, implemented recursiveoperator->resolution across intermediate wrapper objects with cycle detection and a 128-hop recursion budget. - In
src/codegen/expr_unary.rs,lvalue.rs, andexpr_call_member.rs, lowered operator calls and routed receiver evaluations through overloaded operators.
- Enabled parsing and semantic resolution for unary dereference
- Lambda expressions:
- Added
Expr::LambdaAST node and parsing for capture lists ([],[x],[&x],[=],[&]), parameters, trailing return types (-> T), andmutable. - In
src/sema/expr_lambda.rs, synthesized anonymous closure structs (__lambda_N) with capture member fields, member initializers, andoperator()methods (constby default). - Added automatic return type deduction and static
__invoketrampolines for captureless lambdas, enabling implicit conversion to C function pointers (R (*)(P...)) and unary+decay (+[](){}).
- Added
- Language conformance:
- Adjusted function parameter array syntax (
int arr[],int arr[N]) to pointer types (int* arr) insrc/parser/decl_func.rs. - Preserved global variable initializer call targets through dead-code elimination in
src/codegen/program_prepare.rs. - Supported delegating constructors (
Point() : Point(5) {}) and enforced mutual exclusivity against member initializers. - Allowed arbitrary constant expressions and scoped enum variants in switch case labels.
- Added scoped enums (
enum class) with qualified lookup (Scope::Val) and distinct type identity. - Supported user-defined conversion operators (
operator T() const), respectingexplicitqualification. - Added
&and&&ref-qualified member methods with value-category receiver dispatch and distinct symbol mangling (_R,_O). - Implemented structured bindings (
auto [a, b] = p;,auto&,const auto&) for aggregate structs. - Diagnosed
throwwith a clear message noting that exceptions are not supported on this WebAssembly target.
- Adjusted function parameter array syntax (
Verification
- Added positive and negative test suites across operators, lambdas, structured bindings, conversion operators, and delegating constructors.
- Test corpus reached 300 passing tests with zero unexpected failures.
- Formatting and clippy clean.
2026-10-27: Range-Based for Loops and Documentation Architecture
Context & Motivation
Following completion of the initial compiler subset, work focused on adding high-impact iteration syntax and restructuring project documentation into a standardized reference system.
Work Executed
- Range-based
forloops:- Added
StmtKind::RangeFor { decl, range, body }to the AST and parser. - In
src/sema/stmt_range_for.rs, validated range expressions and desugared loops into scoped standardforloops. - For fixed-size 1D and multi-dimensional arrays (
int[N],int[M][N]), lowered iteration to pointer bounds[__begin, __end). - For classes with
.begin()and.end()methods (such asstd::string), synthesized member call bounds. - Supported by-value (
auto x), by-reference (auto& x), and const-reference (const auto& x) bindings. - Verified RAII destructor cleanups on early
break,continue, andreturn. - Added validation diagnostics for non-iterable types.
- Added
- Documentation architecture:
- Organized documentation into an mdBook hierarchy (
docs/book/) with User Guides and Internals chapters. - Standardized Architecture Decision Records in
docs/adr/. - Consolidated development and testing workflows into
docs/contributing.mdanddocs/book/internals/testing.md. - Established
CHANGELOG.mdand movedROADMAP.mdto the repository root. - Added
#![warn(missing_docs)]tosrc/lib.rsand documented public pipeline entry points.
- Organized documentation into an mdBook hierarchy (
Verification
- Added test coverage in
tests/c_basics/pointers_arrays/range_for_array.cpp,tests/mini_stl/range_for_string.cpp,tests/classes_oop/memory_raii/range_for_raii.cpp, andtests/negative/range_for_non_iterable.cpp. - Verified automated mdBook,
cargo doc, and playground builds in CI.
2026-11-01: C++17 Namespace Aliases
Context & Motivation
Section 2 of the roadmap tracked namespace aliases (namespace NewName = OriginalName;), which were rejected upfront by the parser unsupported validator.
Work Executed
- Removed the parser rejection rule for namespace aliases.
- Added
Decl::NamespaceAliasandStmtKind::NamespaceAliasto AST definitions. - Parsed namespace alias declarations at global, namespace, and block scopes.
- Added transitive alias resolution in
ParserandSemaacross types, functions, variables, andusing namespacedirectives. - Diagnosed invalid alias redefinitions in semantic analysis.
Verification
- Added test coverage in
tests/classes_oop/statics_namespaces/namespace_aliases.cpp. - Updated negative test coverage in
tests/negative/namespace_alias.cpp. - Verified clean suite with 338 passing tests in
cppbolt-testrunner.
2026-11-01: C++17 Extended Casts (reinterpret_cast)
Context & Motivation
Section 2 of the roadmap tracked reinterpret_cast across scalar and pointer types within the ILP32 model. Previously, only static_cast and C-style casts were recognized, while reinterpret_cast was rejected at parse time.
Work Executed
- Added
TokenKind::KwReinterpretCastto the lexer and parser. - Introduced
CastKind(Static,Reinterpret,CStyle) and unified cast expressions intoExpr::Cast { kind, target_type, expr }. - Extended the parser to handle
reinterpret_cast<Type>(expr)with template bracket matching. - Implemented semantic validation for
reinterpret_castinsrc/sema/expr.rs:- Allowed conversions between integer types and pointer types without loss under 32-bit ILP32 semantics.
- Allowed conversions between arbitrary pointer types.
- Handled lvalue reference reinterpretations (
reinterpret_cast<T&>(expr)). - Rejected scalar floating-point value conversions (direct float-to-int or float-to-pointer casts) per C++17 rules.
- Bypassed user-defined conversion operators (
operator T()) and constructors.
- Implemented WebAssembly code generation lowering in
src/codegen/expr.rsandsrc/codegen/memory.rs, generating bit-preserving no-ops or width adjustments (wrapping or extending for 64-bit integers). - Documented all additions and marked Roadmap Section 2 complete.
Verification
- Added end-to-end integration tests in
tests/c_basics/types/reinterpret_cast.cpp. - Added unit tests across lexer, parser, sema, and codegen in
src/. - Verified clean suite with 339 passing tests in
cppbolt-testrunner. - Verified zero warnings with
cargo clippyand satisfied all census constraints.
2026-11-02: Nested Classes, Dependent Types, and Associative Containers (std::map, std::set)
Implemented member classes within structs and templates, the typename keyword for dependent type specifiers, template nested class instantiation, and method overload selection by value category. These additions enabled std::map, std::set, and their self-balancing binary search tree detail::AVLTree in stdlib/.
Summary of Work
- Parsed inner struct and class declarations in
src/parser/structs.rsandsrc/parser/structs_nested.rs, registering nested definitions under their outer qualified paths (Outer::Inner) and flattening them into top-level declarations. - Added
typenamekeyword handling insrc/parser/types.rsandsrc/parser/prefix.rs, requiring qualification (typename Scope::Type) and preserving qualified specifiers across function signatures. - Implemented
instantiate_nested_structsandpopulate_instantiation_member_aliasesinsrc/sema/template_nested.rs, cloning and specializing inner classes with outer template arguments substituted and computing their record layouts. - Added
select_member_overloadinsrc/sema/expr_call_member.rsto score candidate signatures and resolve calls across receiver constness and value categories. - Added eager constructor dependency resolution in
src/sema/constructor.rs, instantiating member subobject and array element constructors before body analysis. - Handled scoped destructor lookups in
src/codegen/function_lifecycle.rsandsrc/codegen/raii.rsvia short and qualified names. - Enabled
tests/mini_stl/map_basic.cpp,tests/mini_stl/set_basic.cpp, and the 20-seed differential fuzzing suitetests/mini_stl/differential_fuzz.cppintests/PASSING.txt.
Verification Results
- Unit and integration tests: 401 corpus tests on disk, 401 passed, 1 expected failure (
vector_brace_init), 0 unexpected failures. All 109 negative tests passed. - Host oracle gate: matched native compiler behavior on all expected files.
- Golden baseline: updated to 294 entries in
tests/golden.txt, all matchingtarget/release/cppbolt-cli. - Census audit: 0 files over 500 lines, 0 functions over 80 lines, 0 functions with more than 5 parameters excluding self.
- Clippy: 0 warnings with
-D warningsacross all targets.
2026-11-02: Nested Class Access Control, Scope Resolution, and Fixed-Point Dependent Aliases
Resolved review findings in nested struct scope tracking, global namespace pollution, type specifier access control, and chained dependent alias resolution.
Summary of Work
- Pushed qualified names onto
active_struct_scopesinsrc/parser/structs_nested.rs, ensuring member aliases declared in inner structs bind to the inner scope rather than the outer class. - Removed unqualified nested struct names from
known_typesinsrc/parser/structs_nested.rs, scoping nested names to their enclosing struct and resolving them viaresolve_active_struct_scope_type. - Implemented
check_nested_type_accessinsrc/sema/lookup.rsand called it during type resolution insrc/sema/template_registry.rs, rejecting external references to private nested types while permitting enclosing classes and parameterized templates. - Replaced the fixed 3-iteration alias expansion loop in
src/sema/template_nested.rswith fixed-point iteration (while changed && iterations < 16), supporting arbitrary alias chain depths. - Guarded member alias injection in
src/sema/template_nested.rsandsrc/sema/template_registry.rs, injectingstd::aliases only for classes declared within thestdnamespace. - Added positive tests for nested scopes and alias chains (
nested_classes_scopes.cpp,template_nested_alias_chain.cpp), and negative diagnostic tests for unqualified leakage and private access (nested_class_unqualified_leak.cpp,nested_type_private_access.cpp).
Verification Results
- Unit and integration tests: 406 corpus tests on disk, 405 passed, 1 expected failure (
vector_brace_init), 0 unexpected failures. All 111 negative tests passed. - Golden baseline: all 294 entries match
tests/golden.txt. - Census audit: 0 files over 500 lines, 0 functions over 80 lines, 0 functions with more than 5 parameters excluding self.
- Clippy: 0 warnings with
-D warningsacross all targets.