SCPP constexpr / consteval engine design
Status: research + first-review draft before implementation
0. Scope and headline
This document proposes how scpp should add a general-purpose
compile-time evaluation engine for real C++-style
constexpr and consteval, with the smallest
architecture that can still solve the first urgent library blocker:
- a real typed
std::format_string<Args...>/std::print/std::printlnsurface, and - compile-time rejection of format/argument mismatches the same way
C++20/C++23
<format>does.12
The core recommendation is:
- keep scpp’s real-C++ spelling rule: reuse
constexpr,consteval,if consteval, andstd::is_constant_evaluated()semantics rather than inventing new syntax;34 - implement compile-time evaluation as a frontend interpreter, not by lowering to LLVM IR or executing host machine code;567
- integrate that interpreter into generic-call monomorphization + semantic checking, not into codegen;
- explicitly ship a documented v1 subset that is already enough for format-string validation, constant-expression initializers, recursive helper functions, and straightforward compile-time parsers.
In short: AST-resident compile-time interpreter,
C++-compatible surface semantics, scpp-honest v1 subset, and a concrete
path to std::format_string<Args...>.
1. Current scpp starting point
1.1 Existing frontend pipeline
Today the compiler pipeline is still very direct:
- parse to the AST,
- monomorphize generic calls,
- run move/dataflow checking,
- lower to LLVM IR.8
That is the right place to add compile-time evaluation: after parsing, before codegen, while full type information is available.
1.2 Existing generic machinery is close, but too narrow
The generic-call path already handles:
- ordinary type-parameter deduction from direct parameter positions
like
T x, and - one special variadic base-class-deduction pattern for tuple-like recursive inheritance.91011
But the current implementation is still fundamentally single-pass and left-to-right. It only binds a template pack when the pack appears in:
- a direct trailing function parameter pack, or
- the existing tuple-style base-class-deduction pattern.12
That fails for the motivating <format> shape:
template<typename... Args>
void print(std::format_string<Args...> fmt, Args&&... args);What must happen is:
- bind
Args...from the laterargs...call arguments, - substitute that pack into the earlier parameter
type
format_string<Args...>, - then validate the first call argument against the now-concrete type.
The current monomorphizer cannot do step 2 because it tries to decide each parameter immediately as it walks past it.13
1.3 Existing AST and parser have no constexpr/consteval model yet
The lexer currently has no KwConstexpr /
KwConsteval tokens, the AST Function node has
no evaluation-mode flag, and statement parsing has no
if consteval support.1415
The old book also explicitly documents the earlier design choice that
scpp had consteval but intentionally omitted
constexpr functions to avoid context-dependent behavior.16
That historical rationale matters, but the same chapter also leaves
room to revisit the choice if a real dual-use compile-time/runtime need
appears. The std::format_string<Args...> case is
exactly that need.17
1.4 Existing constant-evaluation support is intentionally tiny and local
The compiler already evaluates one very small compile-time
sub-language: non-type template arguments for variadic generic types
(int literals, parameter names, and +). The
code itself says this is not a general consteval
mechanism.18
That existing helper is still valuable: it shows the codebase is already comfortable with small, purpose-scoped interpreter logic in the frontend. The new engine should generalize this, not replace it with an LLVM-side solution.
1.5 Existing module artifacts need expansion
Current .scppm writing keeps full source bodies only for
exported generic definitions, via the optional SGEN block,
and current .scppm reading explicitly ignores that block
entirely.1920
That is insufficient for imported constexpr /
consteval code, and it is also not the right long-term
architecture for modules. If user code imports
std::format_string, the importer must be able to evaluate
the exported constructor body and its private helper functions during
semantic checking. The settled direction of this design is therefore:
compile-time-relevant definitions should travel through
.scppm as structured serialized AST, not
as source snapshots to be reparsed later.
2. Research findings from mature systems
2.1 Real C++ surface semantics to copy
The C++ model scpp should follow is straightforward at the surface level:
constevaldeclares an immediate function: every potentially-evaluated call must produce a constant expression, otherwise the program is ill-formed.21constexprdeclares a function that may be evaluated at compile time in constant-expression contexts, but may also run normally at runtime.22if constevalbranches on whether evaluation is happening in a manifestly constant-evaluated context; the true branch is an immediate-function context.23std::is_constant_evaluated()exposes the same distinction to ordinary code.24- C++ constant expressions reject many classes of UB during
compile-time evaluation: calling a non-
constexprfunction, overflowing a checked arithmetic operation, division by zero, out-of-bounds reads, exceeding implementation-defined evaluation limits, and so on.25
For scpp, the most important behavioral split is:
constevalfailure is always a compile error at the call site.constexprfailure is only a compile error when the surrounding context requires a constant expression. Otherwise the call remains a runtime call.
That split is exactly what makes
std::format_string<Args...> possible without forcing
every formatting helper to be compile-time-only.
2.2 How real compilers implement constexpr
The implementation pattern in mainstream compilers is also clear:
- Clang’s long-standing frontend implementation is the AST interpreter
in
ExprConstant.cpp, with an evaluation context, virtual call stack, and configurable depth/step limits.2627 - GCC’s implementation in
cp/constexpr.cclikewise uses frontend evaluation with explicit recursion, loop, and operation limits (-fconstexpr-depth,-fconstexpr-loop-limit,-fconstexpr-ops-limit).28
Clang has also grown a newer bytecode-oriented constant interpreter for some paths, but that is an optimization/maintenance evolution inside Clang, not the minimum pattern scpp should start with.29
The architectural lesson for scpp is:
- do not compile to native code and execute it;
- do not wait until LLVM IR exists;
- do interpret frontend structures with compiler-owned memory, compiler-owned budgets, and compiler-owned diagnostics.
That matches scpp’s existing architecture far better than an IR or JIT solution.
2.3 Adjacent language designs worth learning from
Rust
Rust’s const evaluation is deliberately stricter than normal execution, and the compiler runs it using a virtual machine over MIR, not host execution.303132
Two Rust lessons are especially relevant:
- target-model evaluation matters — Rust documents that compile-time interpretation happens in the environment of the compilation target, not the host.33
- an interpreter is still the right abstraction even when it runs over a lowered IR instead of the AST.
scpp should copy lesson (1) immediately, but for v1 keep the simpler AST-level implementation instead of introducing a MIR solely for constexpr.
Zig
Zig’s comptime is much more pervasive: arbitrary code
can be forced to run at compile time, compile-time-known values are
deeply integrated into the type system, and the same language surface is
used for metaprogramming.34
The good lesson is ambition: Zig shows that compile-time execution is most useful when it can run ordinary control flow and helper functions.
The bad fit for scpp is syntax and philosophy:
- Zig’s model is explicitly
comptime-centric, - scpp’s design rule is no new syntax unless the C++ surface truly cannot express it.35
So scpp should borrow the “ordinary code, ordinary control flow” instinct, but not the Zig surface model.
D
D’s CTFE is closer in spirit to what scpp needs: ordinary functions may be executed at compile time when the context requires it, subject to an implementation-defined supported subset.36
The relevant lesson is not D’s exact semantics. It is the product strategy: shipping a useful, documented subset first is acceptable as long as the unsupported parts fail clearly.
3. Design principles for scpp
From the research and current codebase, the constexpr design should follow these rules.
Reuse standard C++ spellings. scpp should accept
constexpr,consteval,if consteval, andstd::is_constant_evaluated()with semantics intentionally modeled on real C++.373839Keep evaluation in the frontend. The interpreter operates over scpp AST + resolved type information and finishes before LLVM codegen.
Be explicit about the v1 subset. Like other scpp features, constexpr support should be honest about what is implemented now and what is deferred.
Use target semantics, not host semantics. Integer width, pointer size, null representation assumptions, and overflow behavior must match the selected target triple, following Rust’s const-eval rule.40
Treat compile-time UB as a semantic error whenever the engine can see it. If evaluation is required and the interpreter detects overflow, division by zero, out-of-bounds access, use-after-lifetime, or forbidden unsafe behavior, compilation fails.
Ban
[[scpp::unsafe]]from v1 constant evaluation. The first implementation should not attempt “unchecked compile-time execution”. Encountering an unsafe function or unsafe block during required constant evaluation is a compile error.Preserve scpp’s artifact split.
.scppmremains the compile-time artifact boundary;.scpparemains the native-code artifact boundary. But.scppmmust now carry compile-time bodies for exported constexpr-relevant definitions.
4. Recommended top-level design
4.1 Surface syntax and AST additions
Add the following frontend representation changes.
Lexer / parser
Add tokens for:
constexprconsteval
and extend if parsing to recognize:
if consteval { ... } else { ... }if !consteval { ... } else { ... }
AST
Add a function evaluation-mode enum:
enum class FunctionEvalMode {
RuntimeOnly,
Constexpr,
Consteval,
};and store it on Function.
Also add:
bool is_constexpron variable declarations that spellconstexpr, and- an
IfModeonStmt(RuntimeIf,ConstexprIf,ConstevalIf,NotConstevalIf) rather than inventing a separate statement kind.
That keeps the AST evolution small and preserves existing
StmtKind::If handling structure.4142
4.2 v1 supported subset
In scope for v1
The first engine should support compile-time evaluation of:
- scalar literals and scalar locals:
bool,char,int,long, unsigned integer variants already modeled by the frontend;float,double, and the project’s existing floating-point scalar family where already modeled by the frontend/codegen;
- string literals, represented as immutable static byte arrays but
still surface-typed as
const char*for compatibility with current scpp typing/codegen behavior;4344 - null pointers and pointers into immutable static storage created by string literals or constexpr globals;
- fixed-size arrays of constexpr-compatible element types;
- read-only
std::span<const T>values derived from arrays or string literals; - trivial
structvalues whose fields are constexpr-compatible; - selected
classvalues if all of the following hold:- every field is constexpr-compatible,
- construction happens through a
constexprorconstevalconstructor, - no destructor execution is required in v1,
- no field is
mutable, - no unsafe operation is used;
- control flow:
- block
- local declaration
- assignment
ifwhilereturn- recursion
- ordinary function calls to
constexprandconstevalfunctions; - floating-point arithmetic and comparison in checked constant-expression contexts, with compile-time rejection for engine-visible invalid operations such as division by zero in the same spirit as ordinary constant-expression failure;45
- compile-time evaluation of expanded folds / pack-expanded helper code after monomorphization.
That subset is enough for:
- format-string scanning and placeholder parsing,
- bounds-checked character walking over string literals,
- recursion-based helper libraries,
- immediate constructors such as
format_string<Args...>("{}").
Explicitly out of scope for v1
v1 should reject required constant evaluation of:
[[scpp::unsafe]]blocks or unsafe functions;extern "C"calls, module-extern calls whose body is unavailable, and all FFI;new,delete, heap allocation, smart-pointer allocation helpers, and any dynamic lifetime management;- union reads/writes;
- mutation through raw pointers;
- virtual dispatch or any runtime-only dynamic-type mechanism;
- lambdas (can come later once closure objects and capture lifetimes are modeled in the interpreter);
- exceptions / throwing behavior;
- I/O, threading, synchronization, environment access, filesystem access;
- compile-time execution that needs a destructor to run user code;
- loops or recursion that exceed the engine budget.
Budgets
Adopt explicit hard limits modeled on Clang/GCC practice:4647
- max call depth: 512
- max total evaluation steps: 1,000,000
- max iterations of one loop: 262,144
These should be constants in v1, not user-facing CLI flags yet.
4.3 Pack-deduction
fix for format_string<Args...>
Existing failure mode
Today monomorphize_generic_function_call walks the
parameters left-to-right and tries to decide each one immediately.48
That works for:
template<typename T> void f(T x)template<typename... Args> void f(Args&&... args)- tuple-like base-class deduction
but fails for:
template<typename... Args>
void print(format_string<Args...> fmt, Args&&... args);because parameter 0 depends on a pack only bound by parameter 1.
Concrete replacement algorithm
Refactor full-header-form generic-call deduction into a three-stage solver.
Stage A: seed explicit arguments
Exactly as today:
- bind explicit non-pack type arguments,
- bind explicit non-type arguments,
- collect explicitly supplied pack elements if present.
Stage B: scan parameters and emit constraints
Replace the current “bind immediately or give up” logic with a constraint collector.
For each parameter/argument pair, emit one of:
- Direct type binding
- pattern
T - bind
T := arg_type
- pattern
- Direct non-type binding
- existing integer-only non-type logic
- Direct function-parameter-pack binding
- pattern
Args&&... args - bind
Args... := [arg_type_0, arg_type_1, ...]
- pattern
- Base-class deduction binding
- keep the current tuple-like logic as one specialized constraint producer
- Deferred compatibility obligation
- parameter type depends on template parameters or template packs, but this parameter is not itself a deduction source
- record
(param_index, parameter_type_pattern, argument_expr)to be checked later
format_string<Args...> is exactly case (5).
Stage C: finalize bindings, then re-check deferred parameters
After the whole call has been scanned:
- ensure every non-pack template parameter is bound;
- finalize pack bindings;
- substitute the final bindings into each deferred parameter type;
- run ordinary parameter compatibility checking on the deferred pair.
For format_string<Args...>, step (3) produces a
concrete type such as:
format_string<int, const char*>and step (4) then checks whether the actual first argument can
initialize that type — which is where the consteval
constructor fires.
Concrete integration with current code
This is not a greenfield rewrite. It should extend the exact code paths already in place.
- Keep
parse_param_list’s existing rule that a true function parameter pack must still be syntactically last.49 - Keep
deduce_via_base_class_chainunchanged as one constraint producer.50 - Replace the current body of
monomorphize_generic_function_callwith:collect_template_constraints(...)solve_template_constraints(...)check_deferred_template_obligations(...)
- Add a new helper:
Type substitute_type_pack(const Type& pattern,
std::string_view pack_name,
const std::vector<Type>& pack_elems);This is the missing piece for earlier dependent parameter types. The
AST already has Type::template_args plus the
is_pack_expansion sentinel for exactly this style of
symbolic pack reference; the current code just does not substitute it in
arbitrary parameter types yet.51
Why this is the right scope
This fix solves the motivating
format_string<Args...> case without
opening the door to arbitrary C++ template deduction complexity.
It still intentionally does not attempt:
- partial ordering between unrelated template overloads,
- recursive “peel one argument and recurse” deduction inside function bodies,
- deduction from arbitrary user-defined conversions.
It only adds the missing ability to bind first, substitute later.
4.4 Evaluator architecture
4.4.1 Core choice
Decision: implement v1 as a tree-walking AST interpreter in the frontend.
Rejected alternatives:
- LLVM IR interpretation / JIT — too late in the pipeline, host-environment leakage risk, much worse diagnostics, and poor fit for semantic-phase failures.
- New MIR/bytecode just for constexpr v1 — plausible later, but too much infrastructure for the first cut.
- Ad hoc per-feature evaluators — would recreate the current non-type-argument one-off problem at a larger scale.
4.4.2 Module placement
Add a new frontend module, e.g. src/constexpr.cppm,
exporting:
ConstValueConstexprErrorConstexprEngine- small helper APIs such as
evaluate_required_constant_expression(...)andevaluate_immediate_call(...)
The engine is used by:
- generic-call monomorphization,
- movecheck/semantic checking,
- module interface loading (to know which bodies must stay available),
- later constant-expression sites such as
constexprvariables and array bounds.
This is a new subsystem used during semantic checking, not a new backend pass.
4.4.3 Runtime model inside the interpreter
The interpreter needs four things.
1. Compile-time values
Use a tagged union:
enum class ConstValueKind {
Void,
Bool,
Int,
Char,
Pointer,
Array,
Struct,
Class,
Span,
};with target-aware payloads.
2. Virtual storage
Represent memory using compiler-owned objects:
- globals / string literals: immutable static storage
- one frame per function call
- one slot per local / parameter
- subobject access for fields and array elements
Pointers are not raw host addresses. They are
(storage_id, offset, pointee_type) references into this
virtual storage.
3. Evaluation context
Track:
- current mode (
RuntimeCheckOnly,RequiredConstexpr,ImmediateConsteval) - call stack frames
- step counter
- loop-iteration counter
- recursion depth
- current source location for diagnostics
4. Result cache
Cache successful evaluation of:
- constexpr variable initializers,
- immediate constructor calls on string literals,
- monomorphized helper functions called repeatedly by the format validator.
This cache should be keyed by
(function name, concrete argument values/types, target triple).
4.4.4 Module-interface impact
Current .scppm support is generic-only and currently
unread on import.5253
The settled design is to replace that source-snapshot direction with structured AST serialization from the outset.
Structured AST payload
.scppm should gain a structured “compile-time AST”
section containing serialized AST nodes, not source text. That section
should store:
- exported
constexprandconstevaldefinitions; - exported generic definitions;
- any private helper definitions, type definitions, and constant initializers reachable from those exported compile-time-relevant definitions;
- enough symbol/type metadata to reconnect the deserialized nodes to the importing module’s semantic environment without reparsing source text.
Why structured AST now
This is the more ambitious v1 path, but it is the right one.
- It matches the architecture mature module implementations converge on: imported templates and constexpr-evaluable entities are carried as compiler-readable structured frontend state, not as raw source text snapshots to be reparsed by every importer.
- It avoids baking a header-era “reparse source in every consumer” model into scpp’s module system just as the language is adding its first real cross-module compile-time execution story.
- It gives generics and constexpr bodies the same long-term representation boundary.
Recommendation on coexistence vs unification
This document recommends unification, not coexistence:
- the new structured serialization mechanism should become the cross-module representation for both constexpr-evaluable bodies and scpp’s existing exported generic bodies;
- the current
SGENsource snapshot path should be treated as a transitional implementation detail of the reference compiler, not as a second permanent module-interface mechanism.
The reason is architectural, not cosmetic. Imported generic monomorphization and imported constexpr evaluation are both consuming frontend body graphs across a module boundary. Maintaining one source-snapshot path for generics and one structured-AST path for constexpr would duplicate reachability rules, versioning, deserialization, and semantic reattachment logic for two pieces of functionality that are fundamentally the same kind of payload.
4.5 Diagnostics and error taxonomy
Recommendation: add
ConstexprError
ParseError, DataflowError,
CodegenError, and DriverError map cleanly to
the current pipeline.54555657
Compile-time evaluation failures are different enough to deserve their own kind:
struct ConstexprError : std::runtime_error {
SourceLocation loc;
std::vector<ConstexprFrame> stack;
};Why a new error kind is better than reusing
DataflowError:
- the failure may arise from perfectly type-correct code whose only issue is “not evaluable here”;
- users benefit from a constexpr stack trace (“while evaluating
parse_replacement_fieldcalled from …”); - it cleanly separates semantic movement/borrow failures from compile-time execution failures.
The existing CLI/project diagnostic printer already knows how to
print SourceLocation; it only needs one more catch block
and an optional note-emission path.58
Required diagnostics in v1
When constant evaluation is required, the engine should issue hard errors for engine-visible cases such as:
- call to non-
constexprfunction, - call to
constevalin a non-immediate context, - arithmetic overflow in checked code,
- division/modulo by zero,
- out-of-bounds array/span/string access,
- reading an uninitialized virtual slot,
- forbidden unsafe operation,
- step/depth/loop-limit exhaustion,
- missing imported constexpr body.
Where C++ itself says “ill-formed, no diagnostic required”, scpp may still diagnose whenever its engine can see the problem. That is consistent with Clause 4’s general “issue at least one diagnostic” direction and with scpp’s broader safety-first design.596061
4.6
constexpr vs consteval semantics in scpp
consteval
- spelled exactly
consteval - every potentially-evaluated call is immediate
- selected overload must be evaluable at compile time
- failure is a compile error at the call site
- may call other
constevalorconstexprfunctions during evaluation - cannot appear on destructors, allocation functions, or deallocation functions, following C++.62
constexpr
- spelled exactly
constexpr - function is eligible for compile-time evaluation
- if the surrounding context requires a constant expression, evaluation must succeed
- otherwise the call remains an ordinary runtime call
- still participates in overload resolution as an ordinary function declaration
if consteval
- parsed and represented explicitly in the AST
- during compile-time interpretation, the interpreter executes the appropriate branch
- the true branch creates an immediate-function context, matching C++23.63
std::is_constant_evaluated()
Treat this as a compiler-known library intrinsic in v1:
- during compile-time interpretation: returns
true - in ordinary runtime codegen: returns
false
That is enough for the standard-library use cases and mirrors common
C++ implementations, which often define it in terms of
if consteval or a compiler builtin.64
5. Worked example:
std::format_string<Args...>
A scpp-friendly v1 library sketch can look like this:
export module std;
export namespace std {
template<typename... Args>
class format_string {
const char* text_;
public:
consteval format_string(const char* s)
: text_(s) {
detail::validate_format<Args...>(s);
}
constexpr const char* get() const {
return this->text_;
}
};
template<typename... Args>
void print(format_string<Args...> fmt, Args&&... args);
template<typename... Args>
void println(format_string<Args...> fmt, Args&&... args);
} // namespace stdNow consider:
int x = 1;
const char* name = "scpp";
std::print("{} {}", x, name);Under the proposed design:
- parser builds the full-header generic
printtemplate normally; - generic-call monomorphization scans the call;
- parameter 1 (
Args&&... args) bindsArgs... := [int, const char*]; - parameter 0 is recorded as a deferred compatibility obligation with
pattern
format_string<Args...>; - after binding finalization, the earlier parameter type becomes
format_string<int, const char*>; - the first argument is a string literal (
const char*in today’s scpp type model);6566 - ordinary parameter checking sees that this can initialize
format_string<int, const char*>only by calling its constructor; - because that constructor is
consteval, the compiler invokesConstexprEngine::evaluate_immediate_call; - inside that immediate call,
detail::validate_format<int, const char*>(s)runs at compile time; - if the parser finds two automatic replacement fields and the argument pack length is two, evaluation succeeds and the call remains well-formed.
If instead the program writes:
std::print("{} {} {}", x, name);then step 9 throws ConstexprError, with the primary
diagnostic at the call site and notes from the virtual constexpr stack,
e.g.:
main.scpp:12:11: error: consteval construction of 'std::format_string<int, const char*>' failed
note: while evaluating 'std::detail::validate_format<int, const char*>'
note: format string expects 3 arguments, but call supplies 2
That is the exact motivating gap solved end-to-end.
6. Phased implementation plan
Phase A — syntax, AST, and serialization schema
- add lexer tokens for
constexpr/consteval - add
FunctionEvalMode, variableis_constexpr, andif constevalAST flags - parse function/constructor declarations carrying those specifiers
- define a versioned structured
.scppmAST payload format for compile-time-relevant definitions - mark exported generic /
constexpr/constevalroots and compute the reachable helper/type graph that must be serialized with them
Merge/testability:
- parser tests
- AST serialization-schema tests
- reachability tests over exported compile-time definitions
- no evaluator yet
Phase B
— structured .scppm serialization / deserialization
- serialize structured AST nodes for exported generic and constexpr-relevant definitions instead of source snapshots
- deserialize those AST nodes on import and reattach symbol/type identity in the importing compiler session
- replace the current generics-only source snapshot path with the structured representation
- preserve backward rejection with a clear diagnostic for older
.scppmfiles that do not carry the required structured payload - add focused cross-module tests for imported generic bodies and
imported
constevalhelpers
Merge/testability:
- module-interface round-trip tests
- imported generic body tests
- imported constexpr body availability tests
Phase C — generic pack-deduction refactor
- refactor full-header-form generic-call monomorphization into constraint collection + solving
- add deferred compatibility obligations
- add
substitute_type_pack(...)for earlier dependent parameter types - preserve existing tuple/base-class deduction as one constraint producer
- add focused tests for:
template<typename... Args> void f(F<Args...>, Args&&...);- explicit + deduced mixed arguments
- failure diagnostics when the earlier parameter becomes incompatible after substitution
Merge/testability:
- no constexpr engine required yet
- this phase alone should make the
format_string<Args...>parameter shape resolvable
Phase D — minimal constexpr / consteval engine
- add
ConstexprError,ConstValue,ConstexprEngine - support scalar locals including floating-point values, string
literals, arrays, read-only spans,
if,while,return, recursion - support checked floating-point arithmetic/comparison alongside integer arithmetic in constant-expression contexts
- reject unsafe/FFI/dynamic-allocation paths explicitly
- wire
constevalcalls into semantic checking - add budget exhaustion diagnostics
Merge/testability:
- immediate functions over ints/chars/floats/strings
- compile-time string parser micro-tests
- imported-module consteval helper tests
Phase
E — constexpr proper and required constant-expression
contexts
- allow
constexprfunctions and constructors - add required constant-expression checking for:
constexprvariable initializers- array bounds once scpp adopts constexpr-sized arrays uniformly
- non-type template arguments (replacing the current tiny one-off evaluator)
- implement
if consteval - implement
std::is_constant_evaluated()intrinsic semantics
Merge/testability:
- same function callable both at compile time and runtime
- branch-selection tests for
if consteval - floating-point constexpr variable / helper tests
Phase F — stdlib
integration for <format> /
<print>
- add
std::format_string<Args...> - port current runtime validator into constexpr-friendly helpers
- route
std::print/std::printlnthrough typed format-string parameters - preserve today’s supported formatting subset first, but move the validation from runtime to compile time
- add user-facing docs/book updates
Merge/testability:
- positive/negative blackbox tests for
std::print/std::println - imported
stdmodule tests
Phase G — widening the constexpr subset
Later, if needed:
- constexpr lambdas
- more class construction/destruction
- more of the standard library
- richer pointer semantics
- optional bytecode/MIR layer if profiling shows AST-walking is a bottleneck
7. Final settled design decisions
User review has now resolved the remaining forks. The final design decisions are:
- Keep v1 class support at the originally recommended
boundary: constexpr-compatible fields plus
constexpr/constevalconstructors, but no user-defined destructor execution in v1. - Adopt structured AST serialization immediately for
.scppmcompile-time payloads, rather than reusing source snapshots. - Unify generic-body and constexpr-body cross-module transport on that one structured serialization mechanism, instead of maintaining two permanent mechanisms.
- Include floating-point constexpr arithmetic in v1, not in a later widening phase.
8. Final recommendation
The practical recommendation is:
- land structured AST serialization/deserialization for
compile-time-relevant
.scppmpayloads first; - land the pack-deduction refactor second;
- land a frontend AST interpreter for
constexpr/constevalthird, with floating-point support already in v1; - use
std::format_string<Args...>as the first proving example; - ship a documented subset instead of waiting for full C++ constexpr parity.
That gives scpp the smallest architecture that is still genuinely
general-purpose, matches real C++ semantics closely enough to be
unsurprising, and directly unlocks the typed std::print /
std::println story that motivated this design.
Sources
cppreference,
std::basic_format_string— https://en.cppreference.com/cpp/utility/format/basic_format_string↩︎cppreference,
consteval— https://en.cppreference.com/cpp/language/consteval↩︎scpp-reference/docs/spec/en/00-front-matter.md, especially Clause 1 and Clause 4.↩︎scpp-reference/docs/old-book/en/ch06-safe-subset.md:142-160.↩︎Clang Users Manual, constexpr limits — https://clang.llvm.org/docs/UsersManual.html#controlling-implementation-limits↩︎
GCC C++ dialect options,
-fconstexpr-*limits — https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Dialect-Options.html↩︎Rust compiler dev guide, const eval overview — https://rustc-dev-guide.rust-lang.org/const_eval.html↩︎
scpp-reference/src/driver.cppm:660-701.↩︎scpp-reference/src/movecheck.cppm:6828-6935.↩︎scpp-reference/src/movecheck.cppm:6202-6235.↩︎scpp-reference/docs/old-book/en/ch05-static-checks.md:368-379, 473-516.↩︎scpp-reference/src/movecheck.cppm:6828-6935.↩︎scpp-reference/src/movecheck.cppm:6828-6935.↩︎scpp-reference/src/lexer.cppm:40-126,240-276.↩︎scpp-reference/src/ast.cppm:553-724.↩︎scpp-reference/docs/old-book/en/ch06-safe-subset.md:142-160.↩︎scpp-reference/docs/old-book/en/ch06-safe-subset.md:142-160.↩︎scpp-reference/src/movecheck.cppm:6166-6200.↩︎scpp-reference/src/driver.cppm:86-121.↩︎scpp-reference/src/driver.cppm:211-243.↩︎cppreference,
consteval— https://en.cppreference.com/cpp/language/consteval↩︎cppreference,
constexpr— https://en.cppreference.com/cpp/language/constexpr↩︎cppreference,
if/ consteval if — https://en.cppreference.com/cpp/language/if↩︎cppreference,
std::is_constant_evaluated— https://en.cppreference.com/cpp/types/is_constant_evaluated↩︎cppreference,
constant expression— https://en.cppreference.com/cpp/language/constant_expression↩︎Clang Users Manual, constexpr limits — https://clang.llvm.org/docs/UsersManual.html#controlling-implementation-limits↩︎
LLVM/Clang source,
clang/lib/AST/ExprConstant.cpp— https://github.com/llvm/llvm-project/blob/main/clang/lib/AST/ExprConstant.cpp↩︎GCC C++ dialect options,
-fconstexpr-*limits — https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Dialect-Options.html↩︎Clang Constant Interpreter docs — https://clang.llvm.org/docs/ConstantInterpreter.html↩︎
Rust Reference, constant evaluation — https://doc.rust-lang.org/reference/const_eval.html↩︎
Rust compiler dev guide, const eval overview — https://rustc-dev-guide.rust-lang.org/const_eval.html↩︎
Rust compiler dev guide, interpreter over MIR — https://rustc-dev-guide.rust-lang.org/const_eval/interpret.html↩︎
Rust Reference, constant evaluation — https://doc.rust-lang.org/reference/const_eval.html↩︎
Zig language reference, comptime — https://ziglang.org/documentation/master/#Comptime↩︎
scpp-reference/docs/spec/en/00-front-matter.md, especially Clause 1 and Clause 4.↩︎D language spec, CTFE — https://dlang.org/spec/function.html#ctfe↩︎
cppreference,
consteval— https://en.cppreference.com/cpp/language/consteval↩︎cppreference,
if/ consteval if — https://en.cppreference.com/cpp/language/if↩︎cppreference,
std::is_constant_evaluated— https://en.cppreference.com/cpp/types/is_constant_evaluated↩︎Rust Reference, constant evaluation — https://doc.rust-lang.org/reference/const_eval.html↩︎
scpp-reference/src/ast.cppm:553-724.↩︎scpp-reference/src/ast.cppm:468-525.↩︎scpp-reference/src/movecheck.cppm:1368-1380.↩︎scpp-reference/src/codegen.cppm:2905-2915.↩︎cppreference,
constant expression— https://en.cppreference.com/cpp/language/constant_expression↩︎Clang Users Manual, constexpr limits — https://clang.llvm.org/docs/UsersManual.html#controlling-implementation-limits↩︎
GCC C++ dialect options,
-fconstexpr-*limits — https://gcc.gnu.org/onlinedocs/gcc/C_002b_002b-Dialect-Options.html↩︎scpp-reference/src/movecheck.cppm:6828-6935.↩︎scpp-reference/src/parser.cppm:2253-2273.↩︎scpp-reference/src/movecheck.cppm:6202-6235.↩︎scpp-reference/src/ast.cppm:159-173.↩︎scpp-reference/src/driver.cppm:86-121.↩︎scpp-reference/src/driver.cppm:211-243.↩︎scpp-reference/src/parser.cppm:17-28.↩︎scpp-reference/src/movecheck.cppm:19-25.↩︎scpp-reference/src/codegen.cppm:35-41.↩︎scpp-reference/src/driver.cppm:38-42.↩︎scpp-reference/src/cli/cli.cppm:190-224.↩︎scpp-reference/docs/spec/en/00-front-matter.md, especially Clause 1 and Clause 4.↩︎cppreference,
constexpr— https://en.cppreference.com/cpp/language/constexpr↩︎cppreference,
constant expression— https://en.cppreference.com/cpp/language/constant_expression↩︎cppreference,
consteval— https://en.cppreference.com/cpp/language/consteval↩︎cppreference,
if/ consteval if — https://en.cppreference.com/cpp/language/if↩︎cppreference,
std::is_constant_evaluated— https://en.cppreference.com/cpp/types/is_constant_evaluated↩︎scpp-reference/src/movecheck.cppm:1368-1380.↩︎scpp-reference/src/codegen.cppm:2905-2915.↩︎