Metaprogramming

Source references link to 01c0f52b (main) in the Logos repository.

Scope: compile-time metaprogramming — macros, metacall, quotation/antiquotation, templates, annotations, cfg, reflection intrinsics, and the Writ/WAny/AnyVal carriers and format machinery that back them. Auto-assembled from the domain: metaprog rules in tools/spec-extract/rules/ (grammar, sema, mono, and codegen layers); each rule id is a permanent linkable address.

Discovery

metaprog.discovery.entry-body-skipped — Entry-file function bodies are skipped during discovery

During the metaprog discovery pass, function bodies of the entry file are not lowered (metaprog_mode); only signatures/items needed for trigger discovery are processed. Name mangling during discovery must stay consistent with the final pass.

Uncertainty: Exact set of skipped work is described only via comment.

Evidence: src/compiler/metaprog_dispatch.hpp#L94-L95, src/compiler/metaprog_dispatch.hpp#L82-L87

Handler triggers

metaprog.trigger.annotation-scan — top-level annotations matching a registered handler trigger are recorded as metaprog targets

After all modules complete field/variant/signature collection (so the metaprog-handler trigger registry is final), the compiler scans top-level ANNOTATION items in user (non-binary) ASTs; one or more consecutive annotations immediately preceding a non-annotation item, whose name matches a registered handler trigger, are each recorded as an (ast-index, item-byte-offset, trigger-name) metaprog target for the driver to dispatch against that following item.

Evidence: src/compiler/sema_collect.cpp#L577-L626

Metaprog dispatch fixpoint

metaprog.dispatch.fixpoint-iteration — Metaprog discovery iterates to fixpoint, capped at 16

Metaprogram item-generation runs as a discovery loop: each iteration re-lowers and fires triggers/metacalls that may emit new items; the loop repeats until an iteration emits nothing new (fixpoint), bounded by a hard cap of 16 iterations.

Uncertainty: Header doc comment; the loop body lives in the .cpp. Behavior of exceeding the cap (error vs silent stop) is not specified here.

Evidence: src/compiler/metaprog_dispatch.hpp#L94-L100

Handler registration

metaprog.handler.register — #[metaprog_handler(“trigger”)] registers a hook

#[metaprog_handler("trigger")] on a function registers (trigger, fn_name); the trigger is the annotation’s first positional string-literal argument. The host driver later scans user items for a matching #[trigger] annotation and invokes the registered fn on each. A missing/unresolvable trigger string is recorded as the sentinel <missing> so a later validation pass can surface the misuse as a diagnostic.

Divergence: A6: metaprog handler registration, Logos-only mechanism.

Evidence: src/compiler/sema_collect.cpp#L1837-L1873

Derive hooks

metaprog.derive.no-rust-derive-syntax#[derive(...)] is rejected; use per-trait triggers

The Rust-style #[derive(Trait, ...)] attribute (a derive annotation carrying args) is not Logos surface syntax and is an error. Logos uses one trigger annotation per derive, #[derive_<trait>], paired with an in-scope #[metaprog_handler("derive_<trait>")] function.

Divergence: Logos replaces Rust #[derive(...)] with #[derive_<trait>] + #[metaprog_handler].

Evidence: src/compiler/sema_impl.hpp#L1762-L1774

metaprog.derive.trigger-may-emit-items — Derive/handler hooks emit sibling items into the program

An annotated item bearing a #[derive_*] / metaprog-handler attribute invokes the corresponding handler during discovery; the handler may synthesize and splice additional top-level items into the program (alongside the annotated item).

Evidence: src/compiler/metaprog_dispatch.hpp#L94-L98, src/compiler/metaprog_dispatch.hpp#L1-L2

Templates

metaprog.template.decl — Template declaration

template <item> wraps a struct/enum/datatype/trait/impl/fn declaration as inert data (an AST blob) rather than a real binding; the inner names are never registered, so referencing the template as a type yields an unknown-type diagnostic. Templates are consumed by metafunctions via apply/metacall.

template struct Pair<A,B> { a: A, b: B }

Divergence: No Rust equivalent.

Evidence: tools/peg_gen/grammars/logos.peg#L604-L612

Annotations (attributes)

metaprog.annot.accumulate-until-item — Annotations and doc-comments accumulate until next item

Annotation nodes accumulate in a pending buffer and are consumed by the next non-annotation item; doc-comment lines/blocks (///, /**, //!, /*!) similarly accumulate. Both buffers are cleared after each item.

Evidence: src/compiler/sema.cpp#L7304-L7305, src/compiler/sema.cpp#L7424-L7458, src/compiler/sema.cpp#L8055-L8058

metaprog.annot.builtin-target-validity — Builtin attribute must match its allowed targets

A builtin attribute is valid only on item kinds in its allowed target set; applying a builtin attribute to a target outside that set is an error. Non-builtin attribute names are not diagnosed at this phase (may be user annotations, cross-module metaprog triggers, or deferred typo checks).

Evidence: src/compiler/sema_impl.hpp#L1775-L1790

metaprog.annot.eq-value-first-field — #[A = lit] binds first field

An annotation #[A = lit] maps the single value to the annotation datatype’s first declared field; if the datatype has no fields it is an error (‘annotation takes no arguments’).

#[A = 5]

Evidence: src/compiler/sema.cpp#L7252-L7261

metaprog.annot.float-suffix-strip — Float annotation literal: strip underscores and f32/f64 suffix

A float annotation literal has all ‘_’ separators removed and an optional trailing ‘f32’/‘f64’ suffix stripped before parsing to a double.

#[A(3.14f64)]
#[A(1_000.5)]

Evidence: src/compiler/sema.cpp#L7173-L7180

metaprog.annot.literal-kinds — Annotation argument literal kinds

An annotation argument literal is one of: integer, float, bool, string, enum-variant (Name::Variant), or array of literals. Arrays are parsed recursively element-by-element.

#[A(42)]
#[A("s")]
#[A(Color::Red)]
#[A([1, 2, 3])]

Evidence: src/compiler/sema.cpp#L7165-L7229

metaprog.annot.named-by-field-name — Named annotation args bind by field name

A named argument key=value in #[A(...)] binds to the field whose name equals the key; an unknown field name is an error (‘has no field’) but does not abort parsing of remaining args.

#[A(name = 3)]

Evidence: src/compiler/sema.cpp#L7271-L7281

metaprog.annot.no-args-bare — Bare annotation #[A] takes no arguments

An annotation written #[A] (no ARGS, no VALUE) produces an instance with an empty argument list.

#[A]

Evidence: src/compiler/sema.cpp#L7249-L7250

metaprog.annot.positional-by-order — Positional annotation args bind by declaration order

Positional arguments in #[A(arg, ...)] bind to the annotation datatype’s fields in declaration order; supplying more positional args than fields is an error (‘takes at most N positional args’).

#[A(1, 2)]

Evidence: src/compiler/sema.cpp#L7282-L7292

metaprog.annot.string-escapes — String annotation literal escape decoding

A quoted string annotation literal has surrounding quotes stripped and escapes \n \t \r \ " \0 decoded; an unrecognised escape is preserved verbatim (backslash + char). A raw string r"..." strips the r" prefix and trailing quote with no escape processing.

#[A("line\n")]
#[A(r"raw\n")]

Evidence: src/compiler/sema.cpp#L7186-L7217

metaprog.annot.struct-flags-set-only — Structural struct flags are set-only (monotonic)

Structural attributes #[zone_mut], #[zoned2], #[rel_ptr], #[self_describing], #[borrow_carrying] each set the corresponding boolean struct flag when present; a present flag can only set, never clear, the base value.

Evidence: src/compiler/sema.cpp#L7344-L7360

metaprog.annot.tag-dispatch-on-trait — #[tag_dispatch(system)] on trait sets dispatch system name

#[tag_dispatch(system_name)] on a trait records system_name as the trait’s TAG_DISPATCH_SYSTEM, identifying the tagged-dispatch system the trait participates in.

Evidence: src/compiler/sema.cpp#L7367-L7376

metaprog.annot.type-code-not-on-generic#[type_code] forbidden on generic items

#[type_code] may not be applied to an item that has type parameters; a type code must be assigned per concrete instantiation, not on the generic definition.

Evidence: src/compiler/sema_impl.hpp#L1792-L1797

metaprog.annot.type-code-on-struct — #[type_code=N] on struct/datatype sets type code

#[type_code=N] on a struct or datatype sets the type’s TYPE_CODE to N and registers N under the fully-qualified name (pkg::Name) in the explicit-type-code table so type_code_of::<T>() resolves cross-package.

#[type_code=42] datatype Foo { }

Evidence: src/compiler/sema.cpp#L7313-L7323

metaprog.annot.type-code-on-template-genos-forbidden — #[type_code] forbidden on parametric (template) genos

#[type_code=N] on a parametric genos (trait with non-empty type params) is an error; the code must be attached to a concrete specialization, else every specialization would collide in the same tag-system slot.

Evidence: src/compiler/sema.cpp#L8040-L8048

metaprog.annot.type-code-on-trait-genos — #[type_code=N] on trait marks it a genos

#[type_code=N] on a trait sets the trait’s TYPE_CODE: the code names the logical datatype family, and each impl Trait for Eidos propagates it to the target struct during lowering.

Evidence: src/compiler/sema.cpp#L7377-L7383

metaprog.annot.type-code-reserved-range — type_code values 1..128 reserved for stdlib

Explicit #[type_code=N] values with 1 <= N <= 128 are reserved for stdlib primitive tags (TypeTagSystem); user code outside package std/std.* using a reserved value is warned and should use codes >= 129.

Evidence: src/compiler/sema_impl.hpp#L1798-L1811

metaprog.annot.user-annotation-requires-marker — User annotation NAME must be an #[annotation]-marked datatype

An annotation whose name is not a compiler-internal key is treated as a user annotation only if NAME resolves to a registered datatype carrying the IS_ANNOTATION_TYPE marker; otherwise it is silently ignored.

Related: metaprog.annot.struct-flags-set-only

Evidence: src/compiler/sema.cpp#L7327-L7336

metaprog.annot.value-int-or-enum-variant — Attribute = value must be an integer or enum variant

The right-hand value of an attribute #[name = V] read as an integer must be either an integer literal (#[name = 123]) or an enum variant path (#[name = Enum::Variant], resolving to that variant’s discriminant). Any other value form is an error; an unknown enum or unknown variant is also an error.

#[type_code = 42]
#[type_code = Tag::Foo]

Evidence: src/compiler/sema_impl.hpp#L1372-L1398

Conditional compilation (cfg)

metaprog.cfg.attr-multi-arg-implicit-and — cfg attribute multi-arg implicit AND

In #[cfg(...)] attribute position, a top-level multi-argument list is an implicit AND of its arguments; #[cfg] with no args matches (true).

Divergence: Multi-arg implicit AND matches Rust (noted inline).

Evidence: src/compiler/sema.cpp#L3654-L3673

metaprog.cfg.bare-flag-predicates — cfg bare-identifier flag resolution

A cfg bare identifier matches as: unix/windows against target_family; test/debug_assertions against the feature set; otherwise as a feature-like flag checked against the active feature set.

Evidence: src/compiler/sema.cpp#L3519-L3528, src/compiler/sema.cpp#L3582

metaprog.cfg.cfg-attr-splice — cfg_attr predicate-gated attribute splicing and item drop

#[cfg_attr(pred, attrs...)] activates first: when pred is true its wrapped attrs are spliced into the item’s annotation list, when false the entry is dropped (wrapped attrs are NOT re-fed to cfg_attr). After activation, every plain #[cfg(...)] is evaluated; an item is dropped from compilation iff any of its cfg predicates is false. The drop gate applies uniformly to both the collection and lowering walks.

Evidence: src/compiler/sema.cpp#L3606-L3652

metaprog.cfg.combinators — cfg all/any/not combinators and boolean literals

cfg predicates compose: all(p...) is the AND of its children, any(p...) the OR, not(p) requires exactly one child and negates it (else error/false). The literals cfg(true)/cfg(false) evaluate to true/false directly. Unknown combinators evaluate to false / raise an error in attribute position.

Divergence: cfg(true)/cfg(false) per Rust 1.80 RFC 3695 (noted inline).

Evidence: src/compiler/sema.cpp#L3553-L3582, src/compiler/sema.cpp#L3692-L3708, src/compiler/sema.cpp#L3721-L3737

metaprog.cfg.key-value-predicates — cfg key=value predicate resolution

A cfg key=value predicate matches against compile-target metadata: target_arch, target_os, target_endian, target_family, target_pointer_width resolve to the host/target platform values; feature = "name" matches iff name is in the active feature set. Any unknown key evaluates to false.

Divergence: Unknown-key-false matches Rust per inline comment.

Evidence: src/compiler/sema.cpp#L3507-L3517, src/compiler/sema.cpp#L3572-L3575

metacall — explicit compile-time evaluation

metaprog.metacall.args-ctfe-constant — Every argument of a metacall call form must be a compile-time constant

For the call form, each argument expression must be CTFE-evaluable to a constant literal; an argument that cannot be folded is a compile error. CALL stores arguments as a flat array, while GENERIC_CALL/STATIC_CALL wrap them as { ITEMS: [...] }.

Divergence: A1/A6: CTFE of metacall args; replaces Rust const-eval.

Related: metaprog.metacall.const-resolver

Evidence: src/compiler/sema_expr.cpp#L17334-L17359

metaprog.metacall.block-tail-required — metacall block must end in a tail expression

A metacall { ... } block must terminate with a tail expression (no trailing semicolon) so the metacall yields a value; a block lacking a tail expression is a compile error. The block’s value type is the type of that tail expression.

Divergence: A1/A6.

Related: metaprog.metacall.return-type

Evidence: src/compiler/sema_expr.cpp#L17366-L17389

metaprog.metacall.const-resolver — metacall argument CTFE resolves bare module-const idents

CTFE of metacall arguments and operands resolves a bare identifier naming a module-level const (collected into the module const-value map, including cross-package consts) to that const’s value, so expressions like metacall { THRESHOLD + 1 } fold.

Divergence: A1/A6: metacall const folding.

Related: metaprog.metacall.args-ctfe-constant, metaprog.metacall.no-runtime-capture

Evidence: src/compiler/sema_expr.cpp#L17311-L17332

metaprog.metacall.explicit-comptime-call — metacall executes a call at compile time

metacall <call_expr> executes the wrapped call at compile time; at module-item position the JIT’d callee returns an item blob whose emitted items are spliced into the module.

Evidence: tools/peg_gen/grammars/logos.peg#L266, tools/peg_gen/grammars/logos.peg#L277

metaprog.metacall.exprblob-deferred-typing — ExprBlob-returning metacall defers result typing to the post-splice pass

When a metacall returns an ExprBlob (an AST-expression fragment marker), pass-1 typing is deferred: let X: T = metacall foo() accepts any annotated T over an ExprBlob RHS; the actual expression type is recovered after the driver splices the blob and pass-2 sema re-lowers it.

Divergence: A3/A6: ExprBlob is the Logos metaprog AST-fragment return.

Related: metaprog.writ-blob.ast-fragment-recurse

Evidence: src/compiler/sema_expr.cpp#L17400-L17407

metaprog.metacall.forms — metacall expression forms

metacall accepts exactly three operand shapes — a block (metacall { … }), a parenthesized expression (metacall (e)), or a call expression (metacall f(…), including generic f::<T>(…) and static Type::m(…)) — and evaluates its argument at compile time.

Divergence: A1/A6 — Logos addition: metacall is the explicit compile-time evaluation operator, the Logos replacement for const-eval (no implicit const-eval).

Related: metaprog.metacall.block-tail-required

Evidence: tools/peg_gen/grammars/logos.peg#L2731-L2736, src/compiler/sema_expr.cpp#L17084-L17088

metaprog.metacall.item-position — Item-position metacall

metacall <call-expr> ; is legal at module item position (top level). The named metafunction is invoked at compile time and must return an item blob; the items it produces are spliced into the program at the metacall’s position, replacing the metacall item itself.

metacall gen_items::<Foo>();

Evidence: tools/peg_gen/grammars/logos.peg#L581-L582, src/compiler/sema_impl.hpp#L4044-L4049

metaprog.metacall.item-position-splice — Item-position metacall splices synthesized items

A metacall foo(); in item position is evaluated during discovery and its result is spliced as synthesized top-level item(s) into the program.

Evidence: src/compiler/metaprog_dispatch.hpp#L1-L3, src/compiler/metaprog_dispatch.hpp#L97-L98

metaprog.metacall.no-nested-metacall — metacall may not be nested inside another metacall’s operand

A metacall operand (call args, or the inner subtree for the block/expr forms) must not contain another metacall node; metacall is a one-shot lift to compile time whose result is a runtime value and therefore cannot serve as a compile-time argument to an enclosing metacall. Violation is a compile error.

Divergence: A1/A6: metacall replaces Rust const-eval; rule has no Rust analogue.

Related: metaprog.metacall.forms

Evidence: src/compiler/sema_expr.cpp#L17090-L17178

metaprog.metacall.no-runtime-capture — metacall block/expr form cannot capture enclosing runtime locals

In the block and parenthesized-expr forms, every VAR_REF must resolve to a name introduced inside the operand (LET/FOR/FOR_EACH binding, or a match-arm pattern binding), a module-level const, or a known function (concrete or generic). A reference to an enclosing-scope runtime local is a compile error, since the metacall is evaluated at compile time with no access to surrounding locals.

Divergence: A1/A6: compile-time evaluation model specific to metacall.

Related: metaprog.metacall.const-resolver

Evidence: src/compiler/sema_expr.cpp#L17196-L17302

metaprog.metacall.return-type — metacall result type must be primitive scalar, WritStatic, Writ, or ExprBlob

The type produced by a metacall operand must be a primitive scalar (bool; integer kinds i8/i16/i24/i32/i56/i64 and u8/u16/u24/u32/u56/u64; f32/f64; integer/float literal types), a &str / Slice<u8>, WritStatic, Writ (incl. Rc<Writ>), or ExprBlob. Any other result type is a compile error.

Divergence: A1/A6: WritStatic/Writ/ExprBlob returns are Logos additions.

Related: metaprog.metacall.exprblob-deferred-typing, metaprog.metacall.writ-autofreeze

Evidence: src/compiler/sema_expr.cpp#L17408-L17424

metaprog.metacall.runtime-passthrough — metacall lowers as a runtime pass-through until driver-side splice

During sema iterations a metacall lowers to its operand’s lowered value (a pass-through), keeping the in-progress IR valid for borrow/type checks. The driver replaces the metacall AST node with the evaluated literal before the final non-metaprog sema pass, so this pass-through lowering never reaches code generation.

Divergence: A1/A6: compile-time splice model.

Related: metaprog.metacall.return-type

Evidence: src/compiler/sema_expr.cpp#L17606-L17610

metaprog.metacall.writ-autofreeze — Writ-returning metacall auto-freezes to WritStatic and is call-form only

A metacall whose operand returns a (mutable) Writ / Rc<Writ> is auto-frozen: user code observes the spliced value as WritStatic (the lowered expression is retyped to WritStatic). The Writ return type is supported only on the call form (metacall foo()); using it with the block or expr form is a compile error.

Divergence: A6: Writ/WritStatic is a Logos addition.

Related: metaprog.metacall.return-type

Evidence: src/compiler/sema_expr.cpp#L17537-L17568, src/compiler/sema_expr.cpp#L17597-L17603

Item-position metacall

metaprog.metacall-item.args-const-eval — Item-position metacall arguments must be compile-time constants

Every argument in an item-position metacall <callee>(args...) is evaluated via CTFE (with module-level named consts resolvable through lookup_const); an argument that fails to fold to a constant is rejected.

Evidence: src/compiler/sema_expr.cpp#L19991-L20034

metaprog.metacall-item.callee-form — Item-position metacall callee must be a call expression

metacall <expr>; at item position requires <expr> to be a free-function call, a generic (turbofish) call, or a static-method call; any other expression form is rejected.

Evidence: src/compiler/sema_expr.cpp#L19980-L19989

metaprog.metacall-item.return-type — Item-position metacall callee must return QuoteItemBlob or ItemList

The callee of an item-position metacall (after ordinary lowering/type-checking of the inner call) must have return type QuoteItemBlob or ItemList; any other return type is rejected.

Evidence: src/compiler/sema_expr.cpp#L20038-L20047

metaprog.metacall-item.turbofish-wstatic-source — WritStatic turbofish type-args re-rendered as source for the metacall thunk

When building the literal call text for an item-position metacall’s generic/static call, a turbofish type-argument that is a WritStatic literal (Foo::<@{...}>) is re-rendered from its parsed writ-literal AST back into @{...} source syntax, rather than through the ordinary type-to-string path (which would emit the non-reparseable internal form @hs_<hex>).

Uncertainty: Internal @hs_<hex> rendering form is inferred from the comment; the type_str() implementation itself is outside this slice.

Evidence: src/compiler/sema_expr.cpp#L20050-L20126, src/compiler/sema_expr.cpp#L20140-L20151

Macro invocation syntax

expr.macro.fn-style-call — Function-style macro invocation

Function-style macros invoke as name!(…), name![…], or name!{…}; the contents between balanced delimiters are captured as raw source text and re-interpreted at sema time per the callee’s macro kind (#[fn_macro] re-parses as an expression list; #[token_macro] lexes as a TokenStream). In no-struct-lit (condition) position the brace form name!{…} is excluded.

Evidence: tools/peg_gen/grammars/logos.peg#L2743-L2754, tools/peg_gen/grammars/logos.peg#L2550-L2559

Function-style macros (#[fn_macro])

metaprog.fn-macro.arg-passed-as-ast-blob — Each fn-macro argument is passed as its serialized AST subtree (ExprBlob)

Each argument expression of name!(...) is passed to the callee unevaluated as an ExprBlob referencing the serialized AST subtree of that argument; the callee receives the syntax tree, not a runtime value. For the Vec form, all argument ExprBlobs are packed into a Vec<ExprBlob> in source order.

Evidence: src/compiler/sema_expr.cpp#L19028-L19116

metaprog.fn-macro.args-are-expr-list — name!(...) arguments parse as a comma-separated expression list

The raw text between the parentheses of name!(...) is parsed as a comma-separated list of expressions (each becoming one macro ARG). If it does not parse as such, the invocation is rejected.

Evidence: src/compiler/sema_expr.cpp#L18566-L18681

metaprog.fn-macro.builtin-macro-list — Fixed set of compiler-built-in function-style macros checked before user #[fn_macro]s

Function-style macro calls (name!(args) / name![args]) are first checked against a fixed compiler-built-in set — cfg!, line!, column!, file!, include!, include_str!, include_bytes!, env!, concat!, concat_bytes!, stringify!, compile_error! — handled by lower_builtin_macro; a callee name not in this set falls through to resolution against user #[fn_macro]-tagged functions.

Evidence: src/compiler/sema_impl.hpp#L4018-L4029

metaprog.fn-macro.callee-marker-required — name!(...) resolves only #[fn_macro]/#[token_macro] callees

A function-style macro call name!(...) resolves only callees marked #[fn_macro]; a #[token_macro] callee additionally receives its raw source text directly as a str argument.

Evidence: src/compiler/sema_impl.hpp#L2553-L2554

metaprog.fn-macro.callee-must-be-marked — name!(...) callee must be a #[fn_macro] or #[token_macro] fn

A name!(...) invocation resolves name against the function overload set; the callee selected must be a fn annotated #[fn_macro] or #[token_macro]. If name is unknown, or no overload bears such an annotation, the call is rejected. Only macro-annotated fns are callable via name!(...) syntax.

Uncertainty: Resolution is restricted to non-generic funcs_ (generic fn_macro out of scope for this slice).

Evidence: src/compiler/sema_expr.cpp#L18497-L18517

metaprog.fn-macro.cfg-builtin — cfg!(...) is a compile-time built-in predicate

The built-in macro cfg!(...) is evaluated at compile (sema) time to a bool literal, before user fn-macro resolution. It accepts built-in target keys, the boolean combinators all/any/not, and user feature flags supplied via --cfg.

Evidence: src/compiler/sema_expr.cpp#L18489-L18493

metaprog.fn-macro.expr-and-item-forms — Function-style macros resolve to #[fn_macro] fns

name!(args) / name![args] resolves CALLEE against #[fn_macro] functions with argument ASTs passed as expression blobs; name!{...} at item position routes through item-splice (callee returns an item list).

Evidence: tools/peg_gen/grammars/logos.peg#L293-L294

metaprog.fn-macro.item-callee-resolution — Callee resolution and lowering of item-position fn-macro calls

A function-style macro invoked at module item position as name!{...} resolves its callee against #[fn_macro]-tagged functions (mirroring expression-position fn-macro calls) and routes its arguments through the per-site raw-capture arg-blob shim, mirroring metacall’s item-position lowering.

Related: metaprog.fn-macro.item-position, metaprog.metacall.item-position, metaprog.fn-macro.builtin-macro-list

Evidence: src/compiler/sema_impl.hpp#L4050-L4055

metaprog.fn-macro.item-position — Item-position fn-macro invocation

IDENT ! { ... } at item position invokes a function-like macro whose body is captured as raw text; the macro must use brace delimiters at item position (parens/brackets are reserved for expression position). The callee returns a list of items.

my_macro! { struct A; }

Related: metaprog.fn-macro.item-callee-resolution

Evidence: tools/peg_gen/grammars/logos.peg#L573-L574

metaprog.fn-macro.result-is-exprblob-spliced — fn-macro call expands to the ExprBlob it returns

A name!(...) expression has the callee’s return type ExprBlob; at expansion the AST produced by the macro (the returned ExprBlob) is spliced in place of the call site before final sema.

Evidence: src/compiler/sema_expr.cpp#L19118-L19132

metaprog.fn-macro.signature-shapes — Accepted fn-macro/token-macro signatures

A #[fn_macro] callee must have exactly one parameter and signature (ExprBlob) -> ExprBlob (single-arg form) or (Vec<ExprBlob>) -> ExprBlob (N-arg packed form). A #[token_macro] callee must have signature (str) -> ExprBlob. Any other signature is rejected.

Evidence: src/compiler/sema_expr.cpp#L18525-L18555

metaprog.fn-macro.single-arg-arity — Single-arg fn-macro takes exactly one argument

For a callee with signature (ExprBlob) -> ExprBlob, the name!(...) invocation must supply exactly one argument; supplying any other count is an error.

Evidence: src/compiler/sema_expr.cpp#L18536-L18539, src/compiler/sema_expr.cpp#L19017-L19022

Item-position function-style macros

metaprog.fn-macro-item.arg-must-be-ast-node — Each re-parsed fn_macro item arg must be a valid AST node

Each element of the re-parsed RAW_TEXT arg list (for the Vec<ExprBlob> item form) must be a non-null pointer value whose root carries a nonzero AST CODE; otherwise the item is rejected.

Evidence: src/compiler/sema_expr.cpp#L19841-L19855

metaprog.fn-macro-item.callee-must-be-fn-macro — Item-position macro call callee must be #[fn_macro] or #[token_macro]

callee!{ ... } / callee!(...){...} at ITEM position resolves callee in the function-overload set; if no overload is found, or none of the overloads carries #[fn_macro] or #[token_macro], the item is rejected.

Evidence: src/compiler/sema_expr.cpp#L19483-L19508

metaprog.fn-macro-item.param-signature — Item-macro callee parameter shape is fixed per attribute

An item-position #[fn_macro] callee must have parameter list (Vec<ExprBlob>) or (). An item-position #[token_macro] callee must have parameter list (str), (name: str, body: str), or (name: str, params: str, body: str). Any other arity/type combination for the given attribute is rejected.

Evidence: src/compiler/sema_expr.cpp#L19521-L19575

metaprog.fn-macro-item.raw-text-as-expr-list — fn_macro Vec<ExprBlob> item form re-parses RAW_TEXT as a comma-separated expr list

For an item-position #[fn_macro] callee of shape (Vec<ExprBlob>), the macro’s RAW_TEXT is wrapped as fn __f() { __c(<RAW_TEXT>); }, parsed as Logos source, and the resulting call’s ARGS elements are lowered one-for-one into ExprBlob values (each argument’s AST subtree is copied into its own document, tagged with its original AST node CODE) and passed to the callee as Vec<ExprBlob>.

Evidence: src/compiler/sema_expr.cpp#L19769-L19825, src/compiler/sema_expr.cpp#L19838-L19876, src/compiler/sema_expr.cpp#L19887-L19901

metaprog.fn-macro-item.return-type — Item-macro callee must return ItemList or QuoteItemBlob

An item-position #[fn_macro]/#[token_macro] callee’s declared return type must be ItemList or QuoteItemBlob; any other return type is rejected.

Evidence: src/compiler/sema_expr.cpp#L19510-L19519

metaprog.fn-macro-item.zero-arg-arity — Zero-arg fn_macro item form rejects nonzero arg count

An item-position #[fn_macro] callee declared with zero parameters is invoked with the RAW_TEXT re-parsed as a (possibly empty) comma-separated arg list; if that re-parse yields one or more arguments, the item is rejected.

Evidence: src/compiler/sema_expr.cpp#L19773-L19825, src/compiler/sema_expr.cpp#L19827-L19832

Built-in macros

metaprog.builtin-macro.vec-desugarvec![...] desugars via a synthesized vec_from_arr call

The vec![...] built-in macro’s synthesized body is re-parsed as the tail expression of an ephemeral fn __f() -> i32 { <wrap_body> } and lowered in the current context, routing vec![e1, e2, ..] through a synthesized vec_from_arr([e1, e2, ..]) call; a re-parse failure produces error_expr() after a diagnostic.

Evidence: src/compiler/sema_impl.hpp#L4032-L4039

Token macros (#[token_macro])

metaprog.token-macro.raw-text-as-str — token-macro receives unparsed raw text as str

For a #[token_macro] callee, the raw bytes between the delimiters of name!(...) are forwarded verbatim as a single str argument, bypassing expression-list parsing and per-argument AST serialization.

Evidence: src/compiler/sema_expr.cpp#L18576-L18619

Item-position token macros

metaprog.token-macro-item.name-slot-scope — Resource NAME slot required for 2/3-arg token_macro, optional+dropped for 1-arg

resource <name> = h!{...} / resource <name> = h!(<params>){...} supplies the LHS binding as a NAME field on the item node. Targeting the 2-arg (name: str, body: str) or 3-arg (name: str, params: str, body: str) #[token_macro] form without a NAME is rejected. Targeting the 1-arg (body: str) form, a supplied NAME is accepted but the value is discarded — never delivered to the callee.

Evidence: src/compiler/sema_expr.cpp#L19577-L19592, src/compiler/sema_expr.cpp#L19658-L19663

metaprog.token-macro-item.params-slot-scope — Resource PARAMS slot only valid for the 3-arg token_macro form

h!(<params>){...} supplies a PARAMS field carrying the raw params source text. Supplying PARAMS against any callee signature other than the 3-arg (name: str, params: str, body: str) #[token_macro] is rejected; conversely, targeting the 3-arg form without a supplied PARAMS is also rejected.

Evidence: src/compiler/sema_expr.cpp#L19600-L19620

metaprog.token-macro-item.raw-text-verbatim — token_macro item form receives raw block text unparsed

For an item-position #[token_macro] callee of shape (str), (name: str, body: str), or (name: str, params: str, body: str), the full RAW_TEXT bytes of the macro’s block (and, where present, the NAME and PARAMS text) are passed to the callee as opaque str values byte-for-byte — the content is NOT re-parsed as Logos source before the call.

Evidence: src/compiler/sema_expr.cpp#L19630-L19664, src/compiler/sema_expr.cpp#L19670-L19707

Item emission (splice results)

metaprog.item-emit.itemlist-iteration — ItemList-returning item macro/metacall emits each contained item

When an item-position fn_macro/token_macro call or item-position metacall resolves to a callee returning ItemList, the compiler synthesises a void thunk that iterates ItemList.blobs and, for each QuoteItemBlob element, emits it into the surrounding module (substituting captured identifiers) then releases its idents/blobs/cursors buffers.

Evidence: src/compiler/sema_expr.cpp#L19709-L19736, src/compiler/sema_expr.cpp#L19904-L19930, src/compiler/sema_expr.cpp#L20196-L20222

metaprog.item-emit.quoteitemblob-single — QuoteItemBlob-returning item macro/metacall emits a single item

When an item-position fn_macro/token_macro call or item-position metacall resolves to a callee returning QuoteItemBlob (not ItemList), the compiler synthesises a void thunk that emits that single blob into the surrounding module (substituting captured identifiers) then releases its idents/blobs/cursors buffers.

Evidence: src/compiler/sema_expr.cpp#L19737-L19757, src/compiler/sema_expr.cpp#L19931-L19950, src/compiler/sema_expr.cpp#L20223-L20241

Quotation — overview

metaprog.quote.typed-ast-literals — quote_* produce typed AST/Type literals

quote_item! { item* }, quote_expr! { expr }, and quote_ty! { type } are typed literals yielding item-list, expression, and Type AST values respectively. Antiquotation $ident (a Type-valued binding) and $ident... (an Array<Type> binding) are legal only inside quote_ty!.

Evidence: tools/peg_gen/grammars/logos.peg#L268-L274

quote_expr!

metaprog.quote-expr.antiquot-carrier-positions — Antiquots are recognized only in defined AST carrier positions

Antiquots and repetition groups are recognized only within the supported carrier set: VAR_REF, BINOP (lhs/rhs), PAREN/UNARY/CAST/DEREF (value), FIELD_READ (selector + receiver), CALL/METHOD_CALL/STATIC_CALL (callee-name-var, receiver, args), STRUCT_LIT/FIELD_INIT/FIELD_SHORTHAND, ARR_LIT/TUPLE_LIT/BLOCK items, statement carriers (LET, LET_DESTRUCT, EXPR_STMT, TAIL_EXPR, RETURN), and control flow (IF, WHILE, FOR, LOOP, ASSIGN, COMPOUND_ASSIGN). Antiquots in unsupported shapes are not substituted.

Divergence: A3/A6

Uncertainty: The exact carrier set is an evolving implementation surface (slices noted in comments), not a frozen spec.

Evidence: src/compiler/sema_expr.cpp#L16586-L16747

metaprog.quote-expr.antiquot-must-be-in-scope — Antiquot variable in quote_expr! must be a bound local

A #name antiquot inside quote_expr! is an error unless name is a variable in scope at the quote site (“#name — variable not in scope”).

Divergence: A3/A6

Evidence: src/compiler/sema_expr.cpp#L16510-L16514

metaprog.quote-expr.no-nested-repeat — Nested repetition groups are not allowed

A #(...) repetition group may not be nested inside another #(...) group (“nested #(...) repetition not supported”).

Divergence: A3/A6

Evidence: src/compiler/sema_expr.cpp#L16589-L16597

metaprog.quote-expr.reify-ast-to-exprblob — quote_expr! reifies an expression AST into an ExprBlob

quote_expr! { e } evaluates to a value of struct type ExprBlob carrying the serialized AST of e. With no antiquots, the AST is emitted as a static rodata blob and wrapped directly as ExprBlob { ptr }.

Divergence: A3/A6 (replaces Rust macro/quote layer)

Evidence: src/compiler/sema_expr.cpp#L16386-L16423, src/compiler/sema_expr.cpp#L16806-L16813

metaprog.quote-expr.repeat-cursor-length-agree — Fixed-length cursors in one repetition group must agree on length

Within a single #(...)* group, all fixed-size [Ident; N] cursors must share the same length N; a sibling cursor with a different N is rejected (“cursor length mismatches sibling cursor in same #(...)*”). A Vec-backed (dynamic) cursor makes the group dynamic and waives the fixed-length agreement check.

Divergence: A3/A6

Evidence: src/compiler/sema_expr.cpp#L16539-L16548

metaprog.quote-expr.repeat-cursor-type — Repetition cursor must be [Ident;N], Vec<Ident>, or Vec<ExprBlob>

A #name antiquot inside a #(...)* repetition group (a cursor) must bind a value of type [Ident; N] (fixed count N), Vec<Ident>, or Vec<ExprBlob> (dynamic count); any other type is rejected (“expected [Ident; N], Vec<Ident>, or Vec<ExprBlob>”).

Divergence: A3/A6

Evidence: src/compiler/sema_expr.cpp#L16523-L16538, src/compiler/sema_expr.cpp#L16469-L16492

metaprog.quote-expr.repeat-needs-cursor — A repetition group must contain at least one cursor antiquot

A #(...)* repetition group body must contain at least one cursor antiquot #x of a cursor type; an empty-cursor body is rejected (“#(...)* body has no cursor #x”).

Divergence: A3/A6

Evidence: src/compiler/sema_expr.cpp#L16600-L16605

metaprog.quote-expr.scalar-antiquot-type — Scalar antiquot must be Ident, or Ident/ExprBlob outside ident-only positions

A #name antiquot outside a repetition group, in a general expression position, must bind a value of type Ident or ExprBlob; in ident-only positions (field names, struct type name, field-read selector) it must bind an Ident. Otherwise it is rejected (“expected Ident” / “expected Ident or ExprBlob”).

Divergence: A3/A6

Evidence: src/compiler/sema_expr.cpp#L16549-L16560, src/compiler/sema_expr.cpp#L16618-L16621, src/compiler/sema_expr.cpp#L16661-L16685

metaprog.quote-expr.subst-runtime — quote_expr! with antiquots substitutes at runtime via logos_quote_expr_subst

quote_expr! containing N>0 antiquots lowers to a block that binds the static template blob and one IdentSpan { ptr, count, kind } per placeholder, then calls logos_quote_expr_subst(template_ptr, size, &spans[0], N) -> *const u8 and wraps the result as ExprBlob { ptr }. Span kind is 0 for Ident slots, 1 for ExprBlob slots, 2 for Vec<ExprBlob> cursors.

Divergence: A3/A6

Evidence: src/compiler/sema_expr.cpp#L16815-L16981, src/compiler/sema_expr.cpp#L16866-L16943

quote_ty!

metaprog.quote-ty.antiquot-type-var — $ident antiquot inside quote_ty! refers to a bound Type value

An ANTIQUOT_TYPE $x inside quote_ty! lowers to a variable reference of type Type (the in-scope binding named x), instead of being reified from a static type.

Divergence: A6 (Logos addition)

Evidence: src/compiler/sema_expr.cpp#L16182-L16184, src/compiler/sema_expr.cpp#L16314-L16316

metaprog.quote-ty.array-antiquot-literal-size — quote_ty! array with antiquot element requires literal integer size

quote_ty! { [$t; N] } lowers to __array_type_apply__(elem_producer, N); the size N MUST be a literal integer (a non-numeric/symbolic size is rejected with “array antiquot requires literal integer size”).

Divergence: A6 (Logos addition)

Evidence: src/compiler/sema_expr.cpp#L16238-L16263

metaprog.quote-ty.generic-inst-antiquot — quote_ty! generic instantiation with antiquot args lowers to __type_apply__

quote_ty! { Foo<args...> } with at least one $ident antiquot among the args lowers to __type_apply__("Foo", [elems]), where each elem is a var-ref (for $x) or a reified Type struct literal (for a concrete type arg). Lifetime args and pack-expand args in this position are rejected (“lifetime / pack args not yet supported”).

Divergence: A6 (Logos addition)

Evidence: src/compiler/sema_expr.cpp#L16299-L16355

metaprog.quote-ty.pack-splice — quote_ty! generic pack-splice lowers to __type_apply__ with runtime array

quote_ty! { Foo<$ts...> }, where the sole generic argument is an ANTIQUOT_PACK $ts..., lowers to __type_apply__("Foo", ts) where ts is a var-ref to a runtime Array<Type>. A pack-splice mixed with any other generic argument (Foo<$t, $ts...>) is rejected (“mixed pack-splice with other args not yet supported”).

Divergence: A6 (Logos addition)

Uncertainty: Mixed-pack rejection is a current implementation limit, not a permanent language rule.

Evidence: src/compiler/sema_expr.cpp#L16271-L16294

metaprog.quote-ty.reify-type-to-struct — quote_ty! reifies a type into a runtime Type value

quote_ty! { T } evaluates to a value of struct type Type whose fields are { kind: u32 = __type_kind_of__::<T>(), name: &[u8] = __type_name_of__::<T>(), size: i64 = size_of::<T>(), align: i64 = align_of::<T>(), uid: u64 = __type_uid_of__::<T>() }.

Divergence: A6 (Logos addition; metaprog reflection intrinsic)

Related: intrinsic.type-reflect.kind, intrinsic.type-reflect.name, intrinsic.type-reflect.uid

Evidence: src/compiler/sema_expr.cpp#L16357-L16383, src/compiler/sema_expr.cpp#L16179

metaprog.quote-ty.tuple-antiquot — quote_ty! tuple with antiquot lowers to __tuple_type_apply__

quote_ty! { ($t1, $t2, ...) } where at least one element is an antiquot lowers to __tuple_type_apply__([p1, p2, ...]) where each pi is the per-element Type producer (var-ref for $x, reified Type literal otherwise); mixed literal/antiquot elements are permitted.

Divergence: A6 (Logos addition)

Evidence: src/compiler/sema_expr.cpp#L16209-L16234

quote_item!

metaprog.quote-item.blob-result-type — quote_item! evaluates to a QuoteItemBlob value

quote_item! evaluates to a QuoteItemBlob struct value with fields { template_ptr, template_size, idents_blob, blobs_blob, cursors_blob }, where template_ptr/template_size address the serialized synthetic-module blob and the *_blob fields carry the packed antiquot substitution data (null when the corresponding placeholder kind has zero occurrences).

Divergence: Logos metaprogramming addition.

Evidence: src/compiler/sema_expr.cpp#L16133-L16144, src/compiler/sema_expr.cpp#L15907-L15910

metaprog.quote-item.cursor-repetition-packing — Cursor (#(...)*) antiquots carry a per-site nesting depth

Each repetition-cursor antiquot site contributes a *const u8 (the address of a Vec cursor variable) plus a parallel per-site depth byte: depth 1 = Vec<Ident>, depth 2 = Vec<Vec<Ident>> (nested #(...)*). The element type is the neutral *const u8; pack reads each cursor according to its depth. When there are no cursor sites, cursors_blob is null.

Divergence: Logos metaprogramming addition.

Related: metaprog.quote-item.blob-result-type

Evidence: src/compiler/sema_expr.cpp#L16056-L16127, src/compiler/sema_expr.cpp#L15939-L15944

metaprog.quote-item.exprblob-antiquot-packing — ExprBlob antiquots are packed by their .ptr field

Each #(expr) antiquot whose lowered expression has type ExprBlob contributes one *const u8 (the ExprBlob’s ptr field) to the blobs blob, in DFS placeholder order; the lowered ExprBlob is bound to a local that outlives the array. When there are no ExprBlob sites, blobs_blob is null.

Divergence: Logos metaprogramming addition.

Related: metaprog.quote-item.blob-result-type

Evidence: src/compiler/sema_expr.cpp#L16005-L16054

metaprog.quote-item.ident-antiquot-packing#name/#(expr) Ident antiquots are packed as Ident pointers

Each scalar Ident antiquot site (#name shortcut or #(expr) yielding Ident) contributes one *const Ident to the idents blob, in DFS placeholder order; a #(expr) form binds the lowered expression to a fresh local whose address is taken. When there are no Ident sites, idents_blob is null.

Divergence: Logos metaprogramming addition.

Related: metaprog.quote-item.blob-result-type

Evidence: src/compiler/sema_expr.cpp#L15953-L16003

metaprog.quote-item.inherit-import-scope — quote_item! inherits the metafn’s import scope

The synthetic module inherits the enclosing metafn’s wildcard use packages, plus a self-use of the metafn’s own package (if non-empty), so that unqualified names inside the quoted items resolve through the metafn’s use-list. Each inherited package becomes one USE node carrying the full dotted package name in NAME.

Divergence: Logos metaprogramming addition; controls hygiene/name resolution of quoted items.

Evidence: src/compiler/sema_expr.cpp#L15821-L15857

metaprog.quote-item.name-antiquot-forms — quote_item! accepts #name and #(expr) name antiquotations

Within quote_item! { ... }, a NAME_VAR placeholder accepts two forms: #name (shortcut) looks the variable up in the metafn scope and requires type Ident; #(expr) lowers the inner expression in the metafn scope and requires type Ident or ExprBlob. Any other pointee kind is an error.

Divergence: Logos metaprogramming construct; no Rust equivalent.

Evidence: src/compiler/sema_expr.cpp#L15569-L15625

metaprog.quote-item.placeholder-walk-balance — Source and destination placeholder counts must match

The number of antiquot placeholders discovered while scanning the source items must equal the number of placeholder slots rewritten in the cloned destination tree; a mismatch is a compile error.

Divergence: Logos metaprogramming addition.

Uncertainty: This is an internal consistency invariant; user-observable only as a diagnostic.

Evidence: src/compiler/sema_expr.cpp#L15797-L15802

metaprog.quote-item.placeholder-walk-order — Placeholder index is fixed by source-tree DFS order

Placeholder indices are assigned by a deterministic depth-first walk of the quoted item subtrees (recursing into all pointer-valued TOM keys and array elements except NAME_VAR), and the destination rewrite mirrors the same recursion so producer indices align with placeholder slots.

Evidence: src/compiler/sema_expr.cpp#L15541-L15661

metaprog.quote-item.repeat-cursor-depth — #(...)* repetition binds cursor placeholders by Vec nesting depth

Inside a #(...)* repetition group, a #name placeholder becomes a Cursor whose depth equals its Vec nesting (1 for Vec<Ident>, 2 for Vec<Vec<Ident>>); the variable’s cursor depth d must be non-zero and ≤ the current repeat depth, else an error. Outside any repetition group, #name must be a scalar Ident.

Evidence: src/compiler/sema_expr.cpp#L15582-L15606, src/compiler/sema_expr.cpp#L15523-L15533

metaprog.quote-item.repeat-nesting-limit — #(...) repetition nesting limited to 2 levels

#(...) repetition groups in quote_item! may nest at most 2 levels deep; deeper nesting is an error.

Evidence: src/compiler/sema_expr.cpp#L15554-L15568

metaprog.quote-item.synthetic-main-module — quote_item! produces a synthetic package main module

quote_item! { item* } constructs a synthetic AST module whose root is MODULE with NAME=“main”, empty PATH_PARTS, ITEMS = the deep-cloned quoted items, and SRC_LINE=1. The result is emitted as a serialized WritStatic blob carried by a QuoteItemBlob value.

Divergence: Logos metaprogramming addition (no Rust equivalent).

Evidence: src/compiler/sema_expr.cpp#L15859-L15894, src/compiler/sema_expr.cpp#L15805-L15819

Antiquotation

metaprog.antiquot.callee-skips-payload-arg — Antiquoted callee drops synthetic first arg

When a call’s callee is produced by antiquotation substitution, the grammar’s bulk $... capture inserts the antiquot payload as the first argument; semantically that first argument is not a real call argument and is skipped.

Uncertainty: Inferred from renderer mirroring lower_call; exact substitution mechanism defined elsewhere.

Evidence: src/compiler/sema_render.cpp#L141-L170

metaprog.antiquot.capture-forms — Writ antiquotation capture syntax

Within a quoted/Writ literal, an antiquotation captures a value either by identifier $name or by expression block ${expr}.

Divergence: Logos metaprogramming antiquotation; no Rust equivalent.

Evidence: src/compiler/sema_render.cpp#L532-L537

Type reflection intrinsics

metaprog.type-intrinsic.apply-generic__apply_generic__ instantiates a template struct type

For __apply_generic__(tmpl_name, arg0..argN), each arg expr is resolved to a compile-time TypeRef via the type-producer-recover protocol (unresolvable arg aborts compilation with a diagnostic); the intrinsic builds LogosType::Struct{name=tmpl_name, type_args=[recovered...]} (pkg inherited from the matching struct def in out_.structs), interns the type, and replaces the call with that type’s Type reflection value.

Divergence: A6

Evidence: src/compiler/mono_clone.cpp#L2191-L2239

metaprog.type-intrinsic.array-type-apply__array_type_apply__(Type, N) builds an array type

__array_type_apply__(elem, size) requires elem to resolve via type-producer-recover (unresolvable is a compile-time abort) and size (after chase) to be a LitInt literal (a non-literal is a compile-time abort); the intrinsic yields LogosType::Array{elem=recovered, arr_size=size}'s Type reflection value.

Divergence: A6

Evidence: src/compiler/mono_clone.cpp#L2244-L2246, src/compiler/mono_clone.cpp#L2342-L2361

metaprog.type-intrinsic.tuple-type-apply__tuple_type_apply__([Type;N]) builds a tuple type

__tuple_type_apply__(arr) requires arg0 to chase to an ArrLit (a non-ArrLit is a compile-time abort); each element is resolved via type-producer-recover (unresolvable element aborts); the intrinsic yields LogosType::Tuple{tuple_elems=[recovered...]}'s Type reflection value.

Divergence: A6

Evidence: src/compiler/mono_clone.cpp#L2244-L2246, src/compiler/mono_clone.cpp#L2316-L2341

metaprog.type-intrinsic.type-producer-recover — Type-producer recovery protocol for type-apply intrinsics

To resolve a LIR sub-expr to a compile-time TypeRef (shared by __apply_generic__/__tuple_type_apply__/__array_type_apply__): (1) chase through VarRef by looking up type_let_inits_, up to 8 hops; (2) if the result is a Call to __typelist_nth__ or __typelist_head__, substitute its type-arg[0] (a type-list) through the current subst map and index into its type_args pack at the literal-int index given as __typelist_nth__'s value arg (__typelist_head__ implicitly index 0); out-of-range index yields no type; (3) if the result is a StructLit with a field named “uid” whose initializer is a Call to __type_uid_of__, recover that call’s type-arg[0] substituted through the subst map; (4) otherwise recovery fails (empty TypeRef).

Divergence: A6

Evidence: src/compiler/mono_clone.cpp#L2152-L2189, src/compiler/mono_clone.cpp#L2255-L2315

metaprog.type-intrinsic.type-reflection-value — Type reflection value shape and uid

A reflected Type value (yielded by __apply_generic__/__tuple_type_apply__/__array_type_apply__) is the struct literal Type{kind:u32=T.kind(), name:&[u8]=type_str(T), size:i64=size_of(T), align:i64=align_of(T), uid:u64} where uid = type_hash_64bit(type_hash_23(type_id_canon(T))); T is registered into out_.type_pool and uid_to_type_[uid]=T so a later __type_uid_of__ lookup on that uid recovers T.

Divergence: A6

Evidence: src/compiler/mono_clone.cpp#L2213-L2238, src/compiler/mono_clone.cpp#L2364-L2387

Enum reflection intrinsics

metaprog.enum-intrinsic.variant-count-of__variant_count_of__::<E>() yields variant count

__variant_count_of__ with type-arg E evaluates to a literal i64 equal to the variant count of E’s enum definition when E resolves to a known LogosType::Enum with a matching def in in_.enums, else 0.

Divergence: A6

Evidence: src/compiler/mono_clone.cpp#L2395-L2417

metaprog.enum-intrinsic.variant-names-of__variant_names_of__::<E>() yields variant names

__variant_names_of__ with type-arg E evaluates to a fixed-size array literal of each variant’s name (string literal), in declaration order; empty array if E does not resolve to a known enum def.

Divergence: A6

Evidence: src/compiler/mono_clone.cpp#L2395-L2407, src/compiler/mono_clone.cpp#L2421-L2425, src/compiler/mono_clone.cpp#L2470-L2477

metaprog.enum-intrinsic.variant-payload-counts-of__variant_payload_counts_of__::<E>() yields per-variant payload arities

__variant_payload_counts_of__ with type-arg E evaluates to a fixed-size array literal of i64, one per variant in declaration order, each equal to that variant’s payload-field count; empty array if E does not resolve to a known enum def.

Divergence: A6

Evidence: src/compiler/mono_clone.cpp#L2426-L2431, src/compiler/mono_clone.cpp#L2470-L2477

metaprog.enum-intrinsic.variant-payload-types-flat-of__variant_payload_types_flat_of__::<E>() yields flattened payload-type reflections

__variant_payload_types_flat_of__ with type-arg E evaluates to a flat fixed-size array literal of Type reflection values (per metaprog.type-intrinsic.type-reflection-value), one per payload type across all variants in declaration order (variant-major, field-minor), each payload type substituted through the current instantiation’s subst map; empty array if E does not resolve to a known enum def.

Divergence: A6

Evidence: src/compiler/mono_clone.cpp#L2432-L2469

Variadic tuple intrinsics

metaprog.variadic.tuple-all-eq__tuple_all_eq__::<T>(a, b) expands to an &&-chain of elementwise eq

__tuple_all_eq__ with type-arg T substituted to a concrete Tuple and exactly 2 args (a, b) expands to the left-associated &&-chain of per-element equality a.i.eq(&b.i): a Tuple-kind element inlines a recursive chain over the nested field refs (never re-emits __tuple_all_eq__, since mono does not re-process synthesized intrinsics); a Slice-kind element (canonicalized to element name “str” when it is Slice<u8>) is compared via a direct 2-arg by-value free-function call; any other element resolves a callee symbol by scanning out_.functions then in_.functions for a name containing <elem-type-str>__eq__f__ at a .-delimited-or-leading boundary (falling back to <elem-type-str>__eq) and calls it as a.eq(&b) via method_call. When T is missing/non-Tuple or fewer than 2 args are present, the intrinsic degenerates to the boolean literal true (also the result for an empty tuple).

Divergence: A6

Uncertainty: Symbol resolution by substring scan is an implementation detail; the observable language rule is the &&-chain elementwise-Eq semantics.

Evidence: src/compiler/mono_clone.cpp#L2488-L2584

metaprog.variadic.tuple-each-field-debug__tuple_each_field_debug__::<T>(self, f) expands variadic tuple Debug

__tuple_each_field_debug__ with type-arg T substituted to a concrete Tuple and exactly 2 args (self, f: &mut Formatter) expands to a fmt_seq-chained Result: fmt_tuple_open(f), then per element i in order: fmt_tuple_sep(f) folded in when i>0, then either (Tuple-kind element) an inline recursive build over the nested field ref, or (otherwise) a Debug-fmt call resolved as a symbol containing <elem-type-str>__Debug__fmt__f__ else <elem-type-str>__fmt__f__ at a .-delimited-or-leading boundary, else literally <elem-type-str>__fmt (elem-type-str = type_str(elem), with Slice<u8> canonicalized to “str”), invoked as field.fmt(f); finally fmt_tuple_close1(f) if the tuple arity is 1, else fmt_tuple_close(f); each step is folded into the running chain via fmt_seq. Every reuse of the shared f argument after its first use goes through a reborrow wrapper (AddrOfTemp(Deref(f))), since &mut Formatter is a move-type under borrow-check and a bare reuse would consume it on the first call. When T is missing/non-Tuple or fewer than 2 args are present, the intrinsic degenerates to a single close-call.

Divergence: A6

Evidence: src/compiler/mono_clone.cpp#L2591-L2683

Writ @-literals

metaprog.writ-lit.capture-zone-alloc-kinds — Capture kinds requiring zone allocation

An @-literal capture requires zone allocation (rather than an inline WAny word) iff its type is F64/F32/FloatLit, a raw pointer (treated as null-terminated C-string), a slice &[u8]/str (ptr+len), or struct StringView; all other (scalar/AnyVal) captures are stored as inline WAny value words.

Evidence: src/compiler/mlir_gen_expr.cpp#L6311-L6324, src/compiler/mlir_gen_expr.cpp#L6404-L6459

metaprog.writ-lit.content-keyed-dedup — Identical @-literal blobs share one global (one address)

Two capture-free @-literals with byte-identical serialized (size-prefixed) content share a single rodata global, and therefore compare equal by address; capture-bearing @-literals are never deduplicated.

Evidence: src/compiler/mlir_gen_expr.cpp#L6199-L6202, src/compiler/mlir_gen_expr.cpp#L6247-L6249, src/compiler/mlir_gen_expr.cpp#L6270-L6271

metaprog.writ-lit.float-capture-widens-to-f64 — Float captures widen to f64

A zone-allocated float capture is stored as f64: an f32 value is widened via float-extension, and a FloatLit (untyped float literal) defaults to f64.

Evidence: src/compiler/mlir_gen_expr.cpp#L6407-L6422

metaprog.writ-lit.size-prefixed-rodata — Capture-free @-literal lowered to size-prefixed rodata

An @-literal (Writ literal) without runtime captures is serialized to a constant blob laid out as [u64 little-endian size][bytes]; the materialized handle points to the payload (8 bytes past the size prefix), so size() reads *(ptr - 8).

Evidence: src/compiler/mlir_gen_expr.cpp#L6151-L6196, src/compiler/mlir_gen_expr.cpp#L6234-L6267

Writ blobs (WRIT_BLOB)

metaprog.writ-blob.ast-fragment-recurse — WRIT_BLOB carrying an AST-category root lowers as that expression

A WRIT_BLOB whose serialized root TinyMap has schema category CAT_AST and whose variant code is a supported expression node (BINOP, LIT_INT, LIT_BOOL, LIT_STR, VAR_REF, CALL, PAREN_EXPR, UNARY, FIELD_READ, METHOD_CALL, CAST, INDEX_READ, STRUCT_LIT, ARR_LIT, TUPLE_LIT, BLOCK, BLOCK_STMT, IF) is lowered by recursively type-checking that root node as an ordinary expression, yielding its recovered expression type. The blob’s arena is retained for the lifetime of sema.

Divergence: A6: Writ/metaprog is a Logos addition (ExprBlob AST fragments spliced from metafunctions).

Related: metaprog.writ-blob.opaque-static-fallback

Evidence: src/compiler/sema_expr.cpp#L17019-L17050

metaprog.writ-blob.opaque-static-fallback — Non-AST WRIT_BLOB lowers to an opaque WritStatic literal

A WRIT_BLOB whose root is null, non-TinyMap, or not of an AST expression category is lowered to an opaque data literal of type WritStatic carrying the raw blob bytes verbatim.

Divergence: A6: WritStatic is a Logos addition.

Related: metaprog.writ-blob.ast-fragment-recurse

Evidence: src/compiler/sema_expr.cpp#L17056-L17060

WAny value encoding

metaprog.wany.bool-value-form — WAny bool value-form encoding

A bool coerced to an 8-byte WAny value word encodes as (b << 8) | 5 (low byte = (WA_BOOL=2)<<1 | 1).

Evidence: src/compiler/mlir_gen_expr.cpp#L6055-L6073

metaprog.wany.identity-passthrough — WAny passthrough and AnyVal zero-extension into WAny

A value already of enum type WAny passes its niche word through unchanged (extracted if struct-typed). A legacy struct AnyVal coerced to WAny is its leading 4-byte word zero-extended to 64 bits (the i24/bool Pod encodings coincide in the low 32 bits).

Evidence: src/compiler/mlir_gen_expr.cpp#L6083-L6104

metaprog.wany.int-i56-value-form — WAny integer value-form (i56 niche)

Any integer type up to 64 bits (i8..i64/u8..u64/i24/u24) coerced to an 8-byte WAny word encodes as (v << 8) | 3 (payload in the high 56 bits, tag = (WA_I56=1)<<1 | 1 = 3).

Evidence: src/compiler/mlir_gen_expr.cpp#L6074-L6082

AnyVal value encoding

metaprog.anyval.bool-value-form — AnyVal bool value-form encoding

A bool coerced to an inline AnyVal word encodes as (b << 8) | 5 where b in {0,1} (low byte = (WA_BOOL=2)<<1 | 1 = 5).

Uncertainty: Bit-layout is a data-substrate (Writ/Memoria) encoding, not a Rust-language construct.

Evidence: src/compiler/mlir_gen_expr.cpp#L6002-L6010

metaprog.anyval.float-ptr-not-inline-capturable — Floats and pointers are not inline-capturable as AnyVal

F32/F64 and pointer/reference types (Ptr/Ref/MutRef) have no inline AnyVal value-form; coercing one yields a null (zero) AnyVal word, and such types must instead use zone-allocated capture encoding.

Evidence: src/compiler/mlir_gen_expr.cpp#L6033-L6040

metaprog.anyval.identity-passthrough — AnyVal value passes through as its first word

Coercing a value already of struct type AnyVal to an AnyVal word extracts its leading word (field 0) unchanged.

Evidence: src/compiler/mlir_gen_expr.cpp#L6041-L6048

metaprog.anyval.int-i24-value-form — AnyVal small-integer value-form (i24 niche)

Any integer type of width <=32 bits (i8/i16/i32/u8/u16/u32/i24/u24) coerced to an inline AnyVal word encodes as ((v & 0xFFFFFF) << 8) | 0x2F (24-bit payload, type tag 0x2F).

Evidence: src/compiler/mlir_gen_expr.cpp#L6011-L6022

metaprog.anyval.int64-truncates-to-i24 — AnyVal 64-bit integer truncates to 24 bits

An i64/u64 coerced to an inline AnyVal word is truncated to its low 24 bits and embedded as the i24 niche ((v & 0xFFFFFF) << 8) | 0x2F; values outside [-2^23, 2^23) lose their high bits in the inline form.

Uncertainty: Lossy truncation is the inline-AnyVal fallback; full-range values require the zone-alloc (C5) path elsewhere.

Evidence: src/compiler/mlir_gen_expr.cpp#L6023-L6032

Format-string parsing (fmt spec)

metaprog.fmt.placeholder-arg-selection — Placeholder argument selection: positional, explicit index, or named

A format placeholder binds to an argument in one of three modes: positional auto-counter ({} consuming 0,1,2,… in order), explicit index ({N}), or named ({name}). Arity validation against positional args applies only when no placeholder uses the named or explicit-index form.

Evidence: src/compiler/sema_fmt.hpp#L55-L66, src/compiler/sema_fmt.hpp#L71-L75

metaprog.fmt.spec-alignment — Format-spec alignment characters

A format placeholder spec may carry an alignment: < = Left, > = Right, ^ = Center; when unspecified the default alignment is chosen per formatting trait.

Evidence: src/compiler/sema_fmt.hpp#L31-L36, src/compiler/sema_fmt.hpp#L46

metaprog.fmt.spec-fill-and-flags — Format-spec fill, sign, alternate, and zero flags

A format placeholder spec supports: a fill char (default space) used with alignment; a sign flag (+ = always show sign, default none); an alternate flag # (emit base prefix 0x/0o/0b); and a zero flag 0 (pad with zeros, overridden by an explicit fill char).

Evidence: src/compiler/sema_fmt.hpp#L38-L42, src/compiler/sema_fmt.hpp#L45-L49

metaprog.fmt.spec-width-precision — Format-spec width and precision

A format placeholder spec may set width and precision; both are unset by default (sentinel -1), and when set precision >= 0 denotes an exact precision.

Evidence: src/compiler/sema_fmt.hpp#L50-L51

metaprog.fmt.string-segmentation — Format string parses into literal text + typed placeholder segments

A format string body (without surrounding quotes) parses into an ordered sequence of segments, each either a literal-text run or a placeholder (carrying its argument selector and spec). Parsing is best-effort: a soft issue emits a diagnostic and continues, while a hard parse error halts at the broken placeholder and sets a failure flag.

Evidence: src/compiler/sema_fmt.hpp#L68-L89

metaprog.fmt.trait-spec-types — Format-spec type characters select a formatting trait

In a format!-family format string, a placeholder’s trailing spec type char selects a formatting trait: no type / absent = Display ({}), ? = Debug ({:?}), x = LowerHex, X = UpperHex, o = Octal, b = Binary, e = LowerExp, E = UpperExp. Each placeholder lowers to a call of that trait’s method on the argument rather than a variadic runtime path.

Uncertainty: LowerExp/UpperExp marked ‘(future)’ in source; presence in enum implies recognized but possibly not yet lowered.

Evidence: src/compiler/sema_fmt.hpp#L20-L29, src/compiler/sema_fmt.hpp#L52

Format-family macros (format!, write!, …)

metaprog.format.arity-check — Format placeholder count must match the argument count

When no explicit {N} index appears, the number of value placeholders in the format string must equal the number of value arguments provided (args after the format string, and after the sink for write/writeln). When explicit indices are used, the number of value arguments must be at least max(explicit-index)+1. Otherwise the macro is rejected.

Evidence: src/compiler/sema_expr.cpp#L18754-L18792

metaprog.format.expansion-shape — Format-family expansion builds a String/Formatter block

format!/format_args_str! expand to a block yielding a String built via a Formatter over a __buf. The print family (println/print/eprintln/eprint) appends a drain of the buffer to stdout/stderr; panic! drains to a panic. write!/writeln! build a Formatter directly over the sink (via (sink).as_formatter()), stream placeholders into it, and yield Result<(),Error> (always Ok — per-placeholder errors are discarded); writeln! additionally writes a trailing newline.

Evidence: src/compiler/sema_expr.cpp#L18828-L18941

metaprog.format.literal-string-checked — Format-family macros validate a literal format string at compile time

For the format family (format, print, println, eprint, eprintln, panic, format_args_str) and the write family (write, writeln), when the format-string argument is a string literal it is parsed and validated at sema time (brace balance, placeholder structure). A non-literal format-string argument skips the check (mirroring Rust format_args!). The format string is arg[0] for the format family and arg[1] for the write family.

Evidence: src/compiler/sema_expr.cpp#L18683-L18728, src/compiler/sema_expr.cpp#L18701-L18722

metaprog.format.placeholder-trait-dispatch — Each format placeholder dispatches to its format-trait method

Each format placeholder lowers to a call dispatching to the format trait selected by the placeholder’s trait kind (e.g. {} -> Display, {:?} -> Debug, plus hex/oct/bin/exp variants), invoked on the argument value with a &mut Formatter. Per-placeholder format spec fields (fill, alignment, sign, alternate, zero-pad, width, precision) are applied to the Formatter before each dispatch and reset between placeholders.

Evidence: src/compiler/sema_expr.cpp#L18843-L18918

metaprog.format.requires-format-arg — Format/write macros require a format-string argument

A format-family or write-family invocation must supply at least the format-string argument; an empty argument list is an error.

Evidence: src/compiler/sema_expr.cpp#L19011-L19014

ABI surface

metaprog.abi.macro-hook-excluded — fn_macro/token_macro hooks excluded from ABI surface

A function attributed #[fn_macro] or #[token_macro] is a compiler-invoked metaprogramming hook (discovered by attribute and invoked by the metaprog driver), not a linkable consumer API entry point; it is flagged as a macro hook so ABI-surface extraction (--emit-abi) excludes it from the public pub-allowlist, preventing its churning signature from tripping the ABI-compat gate.

Evidence: src/compiler/sema_decl.cpp#L1138-L1146


140 rules (138 unique ids) across 32 groups; assembled from tools/spec-extract/rules/** (domain metaprog).