Compile-Time Execution & UDAs
Relevant source files
The following files were used as context for generating this wiki page:
Pop Lang supports a constrained and deterministic system for User-Defined Attributes (UDAs) and Compile-Time Execution (CTE). These features allow developers to attach typed metadata to declarations and execute logic during the compilation phase to produce constants, diagnostics, or specialized code adapters. Unlike some dynamic languages, Pop Lang's compile-time system is capability-limited, budgeting execution to prevent unrestricted host access or non-deterministic builds.
The system is built on three distinct but related pillars:
- UDAs: Immutable typed values attached to declarations using the
@prefix. - Compile-Time Evaluation: Deterministic execution of functions marked with
@CompileTimein a compiler-owned interpreter. - Restricted Introspection: The ability to query symbol handles or types for metadata without enabling broad runtime reflection.
Sources: architecture/10-udas-compile-time-and-reflection.md:3-12, README.md:162-167
User-Defined Attributes (UDAs)
UDAs in Pop Lang are nominal, typed, and immutable values. They are declared similarly to other types and can be attached to various language constructs, such as records, functions, or fields. UDAs are primarily used to provide metadata that the compiler or build tools can consume to alter compilation behavior or generate secondary artifacts.
Declaration and Usage
An attribute declaration defines its parameters, valid attachment targets, and whether it is repeatable on a single target. The @AttributeUsage trusted attribute is used to define these constraints.
@AttributeUsage(
targets = { AttributeTarget.Record, AttributeTarget.Field },
repeatable = false,
)
public attribute Serializable(version: UInt32 = 1)
@Serializable(version = 2)
private record User
name: String
end
Sources: architecture/10-udas-compile-time-and-reflection.md:34-45, architecture/08.1-closed-design-questions.md:144-149
Attribute Value Model
UDA arguments must be constant expressions. The permitted types for these values are strictly limited to prevent runtime objects or system handles from leaking into the compile-time state.
| Permitted Types | Forbidden Types |
|---|---|
| Booleans, numbers, strings, enum cases | Mutable tables or runtime object identity |
| Type and symbol handles (resolved syntax) | Closures with captured mutable state |
| Tuples and immutable records | Raw pointers or foreign handles |
| Immutable arrays of permitted types | Filesystem, network, or clock resources |
Sources: architecture/10-udas-compile-time-and-reflection.md:68-80, architecture/08.1-closed-design-questions.md:151-155
Compile-Time Execution (CTE)
Compile-time execution occurs when the compiler requires the evaluation of a constant expression, such as a UDA argument, a const initializer, or a call to a function marked @CompileTime.
Execution Flow
CTE logic is executed within a compiler-owned interpreter using High-Level Intermediate Representation (HIR). This ensures that behavior remains consistent across different host platforms and backends (e.g., LLVM vs. VM).
The diagram shows the decision point where the compiler shifts into the interpreter for evaluation.
Sources: architecture/10-udas-compile-time-and-reflection.md:88-100, architecture/08.1-closed-design-questions.md:157-160
Capability and Resource Limits
To ensure builds remain deterministic and secure, CTE is heavily sandboxed.
- No Ambient Access: CTE cannot access the filesystem, network, environment variables, or system clocks.
- Budgets: Every evaluation is assigned a "fuel" count for instructions, as well as limits on recursion depth, allocation size, and live values.
- Determinism: Results are cached based on a key containing the function IR, transitive callee fingerprints, and canonical arguments.
Sources: architecture/10-udas-compile-time-and-reflection.md:109-130, AGENTS.md:275-285
Restricted Reflection & Introspection
Pop Lang avoids broad runtime reflection (like "get member by name") to preserve performance and binary strippability. Instead, it provides a "compile-time-first" introspection model.
Introspection Capabilities
Queries are performed using resolved handles rather than string-based lookups. This allows the compiler to verify visibility and existence at compile time.
attribute<<A>>(T): Retrieves a specific attribute of typeAfrom symbolT.hasAttribute<<A>>(T): Returns a boolean indicating if the attribute is present.- No String Mixins: The system cannot parse strings into code or perform
evalat compile time.
Sources: architecture/10-udas-compile-time-and-reflection.md:139-158, architecture/08.1-closed-design-questions.md:175-177
Metadata Retention
By default, UDAs are erased during compilation. They only survive into the runtime if specifically marked for retention (e.g., through generated typed adapters). Runtime reflection is absent unless an explicit resource embedding feature is used.
The diagram represents the relationship between symbols and their compile-time metadata.
Sources: architecture/10-udas-compile-time-and-reflection.md:159-165, architecture/08.1-closed-design-questions.md:165-167
Architecture Invariants
The following constraints are binding for the implementation of Compile-Time Execution and UDAs:
- Backend Neutrality: HIR used in CTE must not contain LLVM-specific objects or layouts.
- No Source Injection: Compile-time code cannot generate source text or tokens for injection into the program.
- Visibility Preservation: Introspection queries must respect standard visibility rules (
public,internal,private). - Deterministic Caching: Evaluations must be invalidated only when transitive inputs change.
Sources: architecture/19-architecture-conformance-and-regression-policy.md:100-110, architecture/README.md:95-101
Conclusion
Compile-time execution and UDAs in Pop Lang provide a powerful yet safe mechanism for metadata-driven development. By strictly limiting capabilities—forbidding string-based source injection and ambient host access—Pop Lang ensures that its compilation process remains deterministic and high-performance across all supported backends. The system prioritizes static correctness, requiring that all metadata and compile-time queries be resolved through typed handles rather than dynamic runtime strings.