Metaprogramming allows programs to participate in generating or transforming other programs. Compile-time metaprogramming shifts some work to the build phase, but different mechanisms operate on different objects: some handle preprocessor tokens, some handle syntax data, some compute constants, and others modify bytecode.
When understanding these concepts, start by asking three questions: Does the metaprogram receive syntax or checked values? At what stage does it execute? Is the artifact code, data, or additional behavior?
Why Regular Functions Aren't Enough
Regular functions are suitable for abstracting runtime calculations; they typically do not need to know the names of the caller's fields and cannot arbitrarily generate new declarations. When the requirement is "generate serialization code for each field of a struct" or "construct a new type based on type parameters," capabilities beyond regular function calls are needed.
However, metaprogramming increases expansion, diagnostic, and build complexity. If regular functions, generics, or composition can clearly express the requirement, they are usually easier to debug. The choice should be based on what information needs to be manipulated, not on which mechanism looks more powerful.
C Macros: Preprocessor Token Expansion
The C preprocessor replaces according to preprocessor token rules; it is not arbitrary character replacement, nor does it understand the types of subsequent complete expressions. It supports uses such as conditional compilation, stringification, and token pasting.
The x, y, and z statements below belong inside a function body; this is a local macro-expansion example.
#definePICK_MAX(a,b)((a)>(b)?(a):(b))int x =3;int y =2;int z =PICK_MAX(x++, y);/* x is evaluated twice, final x=5, z=4 */
Parentheses solve some operator precedence issues, but not repeated evaluation: the comparison uses x++ once, and the selected branch executes x++ again. If arguments contain function calls, side effects may also be duplicated. For fixed-type comparisons, regular inline functions can provide single-parameter evaluation and type checking. See GCC Macro Pitfalls for more expansion boundaries.
Compare "Expanded Twice" vs. "Argument Evaluated Once" at the Same Call Site
Save the following as evaluate-once.c. The two tests start from x=3; the macro results in x=5, result 4; the function results in x=4, result 3. This example does not modify x multiple times in the same unsorted expression, avoiding undefined behavior to explain repeated evaluation.
#include<stdio.h>#definePICK_MAX(a,b)((a)>(b)?(a):(b))staticinlineintmax_int(inta,intb){return a > b ? a : b;}intmain(void){int x =3, y =2;int by_macro =PICK_MAX(x++, y);if(x !=5|| by_macro !=4)return1;
x =3;int by_function =max_int(x++, y);if(x !=4|| by_function !=3)return1;printf("macro=%d function=%d\n", by_macro, by_function);return0;}
Execute cc -Wall -Wextra evaluate-once.c -o evaluate-once, output macro=4 function=3. Using cc -E to view expansion also shows where x++ appears in the two branches. Inline optimization can eliminate function call overhead, but function arguments are still evaluated according to language semantics and do not become macro-style repeated evaluation. GCC Side Effect Duplication
Single evaluation does not equal a fixed evaluation order between multiple arguments. If the second argument also reads/writes x, you must still comply with C's evaluation order rules; if a clear sequence is needed, calculate temporary values with independent statements first, then call the function. Replacing this fixed-type macro with a function solves the argument copying problem, not automatically sorting all expressions.
Why can x increment twice despite correct parentheses?
Track each x++ read and its old/new values, not only the result. When initial x>y, comparison reads once and the selected branch reads again; when x≤y that branch does not execute. The function evaluates one argument value then reuses it. The contrast respects C conditional sequencing rather than exploiting undefined behavior.
Preparing the visual
Why can x increment twice despite correct parentheses?
The macro copies unevaluated expressions; a selected x++ branch executes again after comparison. The function evaluates the argument once and reuses its value. Unselected branches do not execute.
{"id":"macro-evaluation-count","title":"Why can x increment twice despite correct parentheses?","summary":"The macro copies unevaluated expressions; a selected x++ branch executes again after comparison. The function evaluates the argument once and reuses its value. Unselected branches do not execute.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"x\" data-i18n=\"x\"></label><output for=\"x\" id=\"x-value\"></output><input id=\"x\" type=\"range\" min=\"0\" max=\"5\" value=\"3\" step=\"1\"></div><div class=\"control\"><label for=\"y\" data-i18n=\"y\"></label><output for=\"y\" id=\"y-value\"></output><input id=\"y\" type=\"range\" min=\"0\" max=\"5\" value=\"2\" step=\"1\"></div><div id=\"expansion\" class=\"source\"></div><div id=\"evaluations\" class=\"table\"></div><div class=\"metrics\"><div><span data-i18n=\"macro\"></span><strong id=\"macro\"></strong></div><div><span data-i18n=\"function\"></span><strong id=\"function\"></strong></div></div><p id=\"count\" class=\"formula\"></p><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#expansion{min-height:5.4em}.metrics strong{font-size:18px}#evaluations .tr{grid-template-columns:1fr 1fr}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function draw(){const x=+$('#x').value,y=+$('#y').value,branch=x>y,macroResult=branch?x+1:y,macroX=x+(branch?2:1),fn=Math.max(x,y);set('x-value',x);set('y-value',y);$('#expansion').innerHTML='((<mark>x++</mark>) > (y) ? ('+(branch?'<mark>x++</mark>':'x++')+') : (y))\\ncondition: '+x+' > '+y+' = '+branch;table('evaluations',[['compare','read '+x+'; x→'+(x+1)],['selected branch',branch?'read '+(x+1)+'; x→'+(x+2):'read y='+y]]);set('macro',macroResult+' / '+macroX);set('function',fn+' / '+(x+1));set('count',t('count')+': '+(branch?2:1)+' / 1');$('#macro').dataset.values=[macroResult,macroX].join(',');}$('#x').oninput=draw;$('#y').oninput=draw;draw();","audio":false,"strings":{"count":"x++ evaluations (macro / function)","function":"Function: result / final x","heading":"Why can x increment twice despite correct parentheses?","intro":"Compare PICK_MAX(x++, y) and max_int(x++, y). Vary initial x,y and inspect branch selection and evaluation count.","macro":"Macro: result / final x","reset":"Reset","scope":"Fixed C conditional semantics, x/y0..5, side-effect-free second argument. No unsequenced modifications; no claim of fixed evaluation order between function arguments.","scopeLabel":"Model scope and assumptions","x":"Initial x","y":"Unchanged y"}}
Lisp Macros: Input is Unevaluated Code Forms
Lisp code forms can be operated on as data, such as lists. Macros receive forms that are not evaluated according to regular function argument rules, produce new code forms, and then the language processes the expansion result.
Here condition and body are placed into the generated if form; the runtime actions they express are not executed during macro expansion. Quasi-quotation, backquote, and concatenation help construct code, but macro authors still need to consider variable capture, evaluation count, expansion environment, and error locations. Code being data does not mean scope and semantic issues disappear.
Rust Procedural Macros: TokenStream is Not a Typed AST
Rust procedural macros consume and produce TokenStream at compile time. Macros can parse into syntax structures using libraries like syn, but this does not mean obtaining the internal AST after rustc completes type inference, nor can it generally query the type deduced for a specific expression.
Procedural macros are mainly divided into three types: function-like, derive, and attribute, with different insertion or replacement rules for expansion. For example, derive generates additional entries, often used to generate impl; attribute can replace the annotated entry. Interfaces and rules are in the Rust Reference.
Suppose generating a serialization implementation for struct Record<T> { value: T }; the macro must preserve generic parameters and where constraints, generate valid paths and bounds, and then subsequent compilation stages check if the implementation is type-correct. "The macro knows field syntax" is different from "The macro proves field types satisfy all constraints."
Hygiene: Who Generated Names Point To After Expansion
Hygiene solves the accidental capture of generated names and caller names. Rust macro_rules! has its mixed-position hygiene rules; procedural macros cannot be generalized as "naturally fully hygienic." Procedural macro authors need to handle name conflicts and path resolution, such as using appropriate absolute paths, preserving spans, and avoiding generating auxiliary names that are prone to conflicts.
Hygiene does not automatically guarantee single evaluation, correct borrowing, or good error messages. These properties of macro-generated code still need to be designed separately.
Can generated tmp capture the caller’s tmp?
Two tmp spellings may originate in different declaration environments. Caller C and generated M stay visible, with separate spliced and helper uses. Disabling hygiene captures only when the caller also uses tmp; renaming avoids this instance, not all macro collisions. Hygiene maintains binding provenance rather than merely choosing an unusual name.
Preparing the visual
Can generated tmp capture the caller’s tmp?
Hygiene preserves the spliced expression’s binding while generated uses refer to the helper declaration. Nearest-name lookup on expanded text can capture the caller variable.
Rust's const fn can execute in constant contexts that meet conditions; it can also execute at runtime at regular call sites. You cannot assume every call happens at compile time just based on the const in the function declaration.
constfnsquare_table()->[u32;4]{letmut out =[0;4];letmut i =0;while i <4{
out[i]=(i * i)asu32;
i +=1;}
out
}constSQUARES:[u32;4]=square_table();fnmain(){assert_eq!(SQUARES,[0,1,4,9]);}
Constant contexts require obtaining a valid constant result; the language restricts executable operations. Array out-of-bounds become compile errors when evaluation is mandatory; allowed syntax and reference behaviors are specified by language and version. Rust Constant Evaluation
C++ constexpr, consteval, and Zig comptime have different specific capabilities and should not be uniformly described as the same interpreter. Do not confuse const evaluation with the permissions of build scripts or procedural macros: the latter two may access the build environment and file system.
Pre-computation shifts computation time to the build phase and may place results into artifacts. It may also increase binary size, compile memory, or cache pressure; "compile-time execution" does not mean all costs are zero.
Same const fn, Call Context Determines Which Stage Exposes Errors
Below, define the maximum valid payload as 1024, with a fixed header occupying 8 bytes. The initialization of constant PACKET_SIZE must be evaluated at compile time; the length from args in main belongs to regular runtime input.
constfnpacket_size(payload:usize)->usize{assert!(payload <=1024,"payload too large");
payload +8}constPACKET_SIZE:usize=packet_size(16);fnmain(){assert_eq!(PACKET_SIZE,24);let runtime_input =std::env::args().count();assert_eq!(packet_size(runtime_input), runtime_input +8);}
Compiling directly and running without extra arguments will pass. If the constant initialization is changed to packet_size(1025), the compiler must reject this constant; if 1025 is only passed in the regular path of main, the program will panic when executing the assertion. The const fn body does not become two different logics; what changes is whether the current context requires a valid constant result. Constant Context
Usage Location
Stage Obligation
Cannot Infer
const initialization
Must complete under constant evaluation rules
Data has no artifact space cost
Regular runtime call
Produces value or failure according to regular execution semantics
Compiler does not perform constant folding at all
Build script reading file
Runs independent program at build time
File content automatically becomes a declared dependency
Procedural macro generating expression
Produces syntax to be compiled
Expression has completed type checking
At which stage does the same const fn fail?
Separate phase obligations from optimization opportunities. Const initialization requires valid compile-time evaluation; an ordinary path has runtime behavior only when executed. The same packet_size body remains while context and input change. Even if an optimizer precomputes an ordinary call, it cannot indiscriminately replace its semantics with const-context diagnostics.
Preparing the visual
At which stage does the same const fn fail?
A const initializer must yield a valid constant. An ordinary call is not required constant evaluation merely because the function is const; an unexecuted call has no runtime panic.
{"id":"constant-evaluation-context","title":"At which stage does the same const fn fail?","summary":"A const initializer must yield a valid constant. An ordinary call is not required constant evaluation merely because the function is const; an unexecuted call has no runtime panic.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"context\"><button data-mode=\"const\" data-i18n=\"constant\"></button><button data-mode=\"runtime\" data-i18n=\"runtime\"></button></div><div class=\"control\"><label for=\"n\" data-i18n=\"n\"></label><output for=\"n\" id=\"n-value\"></output><input id=\"n\" type=\"range\" min=\"0\" max=\"1050\" value=\"1025\" step=\"1\"></div><label class=\"check\"><input type=\"checkbox\" id=\"execute\" checked><span data-i18n=\"execute\"></span></label><p id=\"source\" class=\"source\"></p><div id=\"phases\" class=\"track\"></div><div class=\"metrics\"><div><span data-i18n=\"compile\"></span><strong id=\"compile\"></strong></div><div><span data-i18n=\"run\"></span><strong id=\"run\"></strong></div><div><span data-i18n=\"value\"></span><strong id=\"value\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#source{min-height:12.6em}.metrics strong{font-size:17px;min-height:3.2em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let mode='const';function draw(){const n=+$('#n').value,valid=n<=1024,constant=mode==='const',executed=$('#execute').checked;$('#execute').disabled=constant;set('n-value',n);set('source','const fn packet_size(n) {\\n assert(n <= 1024); n+8\\n}\\n'+(constant?'const SIZE = packet_size('+n+');':'if execute { packet_size(input_n); }'));const compileOK=!constant||valid,run=constant?(valid?t('embedded'):'—'):!executed?t('notrun'):valid?'return':'panic';set('compile',t(compileOK?'ok':'fail'));set('run',run);set('value',valid&&(constant||executed)?n+8:'—');cells('phases',[{text:'source',cls:'on'},{text:constant?'const eval':'compile',cls:compileOK?'on':'gold'},{text:'runtime',cls:constant?'empty':executed?'on':'empty'}]);$('#compile').dataset.valid=String(compileOK);pressed(mode);}$('#n').oninput=draw;$('#execute').onchange=draw;document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;draw()});draw();","audio":false,"strings":{"compile":"Compilation","constant":"const initializer","context":"Call context","embedded":"Constant embedded","execute":"Execute this call","fail":"Constant-evaluation error","heading":"At which stage does the same const fn fail?","intro":"packet_size(n) checks n≤1024 then returns n+8. Choose context and input to separate compile-time obligations from runtime calls.","n":"Payload length n","notrun":"Not executed","ok":"Compilation succeeds","reset":"Reset","run":"Execution","runtime":"Ordinary runtime call","scope":"Models phase obligations of the article’s Rust const fn, payload0..1050. No rustc in-browser; optimization may fold ordinary calls while preserving observable semantics.","scopeLabel":"Model scope and assumptions","value":"Produced value"}}
Templates and Generics: Parameters Can Also Determine Program Structure
Generic type parameters participate in checking; some languages also support value parameters, type-level computation, or code instantiation based on parameters. Monomorphization allows different instances to obtain specialized implementations, but increases the number of instances and compilation costs. Type erasure and dictionary passing use different runtime representations and calling conventions.
These can be combined with macros: macros first generate implementations with generics, and the compiler then checks and generates code according to strategies. Calling all generics "macros" or calling all metaprogramming "AST replacement" obscures stage differences.
AOP Weaving: Select Join Points in Program and Add Behavior
AspectJ describes a set of join points via pointcut and specifies behavior to execute via advice. call and execution match different objects; you cannot uniformly draw method execution join points as inserting AST before the call point.
AspectJ supports source code compilation and weaving into existing class files; artifacts can be bytecode with woven behavior; there is also load-time weaving. It is not limited to "the compiler only modifies Java AST." AspectJ Compiler and Weaver
Early weaving can avoid certain proxy boundaries, but logging, timing, and checks in advice still execute at runtime, and dynamic join point conditions may also have costs. Whether self-calls are intercepted depends on the mechanism, matching rules, and weaving scope; you cannot use "covers all calls, zero runtime overhead" as a universal conclusion.
Build Cache Must See the Generator's Real Dependencies
For example, generating a serialization implementation for Record from a schema; field order comes from the schema, naming rules from the generator version, and output mode from configuration switches. Listing only the generator source files as dependencies may lead to using old outputs after modifying the schema; listing only the schema as a dependency may fail to rebuild after upgrading naming rules.
Conversely, if the generator unconditionally adds the current time on every run, even if all business inputs remain unchanged, the generation result will change, filling caches and code reviews with noise. First determine if time belongs to the artifact's semantics; if it's just logging, leave it in the build log.
Change
Reasonable Expectation
Need to Verify
New field
Corresponding generation declaration changes
Whether input dependencies cause the generator to rerun
Upgrade generator rules
Same schema generates according to new rules
Whether tool version enters the cache key
No business input change
Generated content remains stable
Order, random numbers, and implicit environment
Generate illegal type combinations
Compilation diagnostics point to domain input
Whether source locations or generation manifests are preserved
Compile-time computation also has scale boundaries: if N generic instances each generate B bytes, code space may increase with the number of instances, but linker folding, optimization, and sharing strategies will change the final size. Look at artifacts and build resources; do not assume compile-time metaprogramming definitely saves total cost just because fewer lines of source code were written.
Engineering Review Quick Reference
Requirement
Mechanism to Consider
Check
Small amount of repeated calculation
Functions, Generics
Is it already sufficient to express
Generate implementation by field
derive / Procedural Macros
Generics, paths, error spans, checkability of generated code
Join point semantics, coverage, runtime side effects
Verification should not only look at normal expansion, but also at error inputs, same-name variables, parameters with side effects, generic boundaries, and whether incremental builds after modification invalidate correctness. Generated results should have traceable locations and stable order, avoiding implicit inputs from the build environment causing unreproducible artifacts.