Proposal: Compile-Time Struct Construction
Status: Approved
Approved: 2026-03-26
Author: Eric (with AI assistance)
Created: 2026-03-26
Affects: Compiler (parser, type system, monomorphization, evaluator, LLVM codegen)
Depends on: approved/compile-time-reflection-proposal.md (compile-time reflection primitives)
Enables: Pure Ori JSON deserialization, ORM mapping, configuration loading, builder patterns
1. Summary
This proposal introduces $construct<T> — a compile-time intrinsic that builds a struct from field/value pairs generated by $for + fields_of(T). It completes the compile-time reflection story by enabling construction in addition to inspection, closing the gap that prevents generic deserialization.
@from_json<T>(json: JsonValue) -> Result<T, JsonError> = {
let obj = json.as_object()?
$construct<T>(
$for field in fields_of(T) yield {
(field, FromJson.from_json(json: obj[field.name] ?? JsonValue.Null)?)
}
)
}
At compile time, this expands to a direct struct literal — zero overhead, fully typed, identical to hand-written code.
2. Problem Statement
The approved compile-time reflection proposal (compile-time-reflection-proposal.md) enables reading struct fields generically via fields_of(T) + $for + splice access value.[field]. This covers serialization, debug formatting, schema generation, diffing, and validation.
But it cannot construct a struct from individually computed field values. This blocks:
- Deserialization — JSON/TOML/YAML text to typed struct
- ORM mapping — database row to struct
- Configuration loading — env vars / config file to struct
- Builder patterns — accumulate fields, produce struct
- Default-with-overrides —
Tfrom defaults plus partial input - Property-based testing — generate arbitrary struct instances
All of these require: “given a type T and a way to produce each field’s value, construct T.”
3. Design
3.1 The $construct<T> Intrinsic
$construct<T>(field_values)
Where field_values is a compile-time-known list of ($FieldMeta, value) pairs, typically produced by $for...yield over fields_of(T).
Semantics: At monomorphization time, when T is concrete, the compiler:
- Evaluates the field list (the
$forexpansion produces N pairs) - Matches each
$FieldMetato the corresponding struct field by name - Type-checks each value against the expected field type
- Emits a direct struct literal:
T { field1: val1, field2: val2, ... }
Zero-cost guarantee: The compiled output is identical to a hand-written struct literal. No intermediate collections, no runtime field lookup, no dynamic dispatch.
3.2 Syntax
$construct<T>(field_values_expr)
The argument to $construct is a single expression that evaluates to a compile-time sequence of ($FieldMeta, value) pairs. Typically this is a $for...yield expression:
$construct<T>(
$for field in fields_of(T) yield {
(field, compute_value(field))
}
)
A list literal also works for concrete types:
let [$name, $age] = fields_of(User)
$construct<User>([($name, "Alice"), ($age, 30)])
Note: For concrete types, a normal struct literal (
User { name: "Alice", age: 30 }) is preferred.$constructis designed for generic contexts whereTis a type parameter.
The $FieldMeta in each pair identifies which field to set. The value is the initializer for that field.
3.3 Type Checking
Each (field, value) pair is independently type-checked:
fieldmust be a$FieldMetafromfields_of(T)(verified at compile time)valuemust be assignable to the type of the referenced field
The type of the whole $construct<T>(...) expression is T.
3.4 Completeness Checking
At compile time, the compiler verifies:
- All fields covered: every public field of
Thas exactly one(field, value)pair - No duplicates: no field appears twice
- No extras: no
$FieldMetathat doesn’t belong toT
Missing fields are a compile error (E0470). This prevents partially-constructed structs.
3.5 Partial Construction with Defaults
For types that implement Default, $construct_partial<T> allows omitting fields — missing fields are filled with their default values:
$construct_partial<T: Default>(
$for field in fields_of(T) if has_json_field(obj, field.name) yield {
(field, FromJson.from_json(json: obj[field.name])?)
}
)
This expands to:
T {
name: FromJson.from_json(json: obj["name"])?, // present in JSON
age: Default.default(), // missing, use default
}
$construct_partial is a separate intrinsic from $construct to make the Default requirement explicit. $construct always requires all fields; $construct_partial allows omissions but requires T: Default.
3.6 Expansion Example
Given:
type User = { name: str, age: int, email: Option<str> }
@from_json_impl (json: JsonValue) -> Result<User, JsonError> = {
let obj = json.as_object()?
$construct<User>(
$for field in fields_of(User) yield {
(field, FromJson.from_json(json: obj[field.name] ?? JsonValue.Null)?)
}
)
}
After monomorphization, this expands to:
@from_json_impl (json: JsonValue) -> Result<User, JsonError> = {
let obj = json.as_object()?
User {
name: FromJson.from_json(json: obj["name"] ?? JsonValue.Null)?,
age: FromJson.from_json(json: obj["age"] ?? JsonValue.Null)?,
email: FromJson.from_json(json: obj["email"] ?? JsonValue.Null)?,
}
}
Identical to hand-written code. Zero overhead.
3.7 Visibility Rules
$construct<T> follows the same visibility rules as struct literal construction:
- All-public fields: Works with any type where all fields are accessible at the call site.
- Private fields: If
Thas private fields (::prefix),$construct<T>cannot be used from outside the defining module —fields_of(T)only returns public fields, so the completeness check (§3.4) will report missing fields (E0470). This is the same restriction as writingT { ... }directly. - Types with private fields must implement construction traits (e.g.,
FromJson) manually, providing access to private fields within the defining module via normal struct literal syntax.
This preserves encapsulation: generic construction only works for types that expose all their structure.
3.8 Compile-Time Expansion Semantics
The $for...yield expression inside $construct is compile-time expansion, not a runtime list. Each iteration is independently unrolled and type-checked. For:
$construct<User>(
$for field in fields_of(User) yield {
(field, FromJson.from_json(json: obj[field.name] ?? JsonValue.Null)?)
}
)
The compiler expands this to three independent pairs:
($name_meta, FromJson.from_json(json: obj["name"] ?? JsonValue.Null)?) // type-checked as str
($age_meta, FromJson.from_json(json: obj["age"] ?? JsonValue.Null)?) // type-checked as int
($email_meta, FromJson.from_json(json: obj["email"] ?? JsonValue.Null)?) // type-checked as Option<str>
Each value expression is checked against its corresponding field type. There is no runtime [($FieldMeta, value)] list — the notation is conceptual. This follows the same expansion model as $for in the compile-time reflection proposal.
4. Alternatives Considered
4.1 Default + Splice Assignment
@from_json<T: Default>(json: JsonValue) -> Result<T, JsonError> = {
let obj = json.as_object()?
let result = T.default()
$for field in fields_of(T) do {
result.[field] = FromJson.from_json(json: obj[field.name] ?? JsonValue.Null)?
}
Ok(result)
}
Pros: Uses only features from the already-approved reflection proposal. No new intrinsic.
Cons:
- Requires
T: Defaulteven when all fields are provided - Each
result.[field] = xdesugars toresult = { ...result, field: x }— creates N intermediate struct copies for N fields - ARC uniqueness analysis may optimize some copies away, but the guarantee is not zero-cost
- Sequential mutation semantics are less clear than declarative construction
Verdict: Acceptable as a user-level convenience pattern but not as the primary construction mechanism. The performance gap matters for the flagship JSON parser use case.
4.2 $build<T> Block Expression
$build<T> {
$for field in fields_of(T) do {
.[field] = compute_value(field)
}
}
Pros: Reads like imperative construction.
Cons:
- Introduces a new block syntax that looks like mutation but isn’t
- The
.[field] = valueinside$buildhas different semantics than outside (accumulation vs assignment) - Harder to compose with
$for...yield(thedoform doesn’t produce values)
Verdict: More complex than $construct<T> for no expressiveness gain.
4.3 Auto-Generated T.from_fields(...)
User.from_fields(name: "Alice", age: 30, email: None)
Pros: Uses existing call syntax.
Cons:
- This is just a struct literal with extra steps
- Cannot express the variadic heterogeneous signature generically
- Doesn’t compose with
$for— you’d need to call it with expanded arguments
Verdict: Doesn’t solve the generic construction problem.
4.4 Struct Literal with $for in Field Position
T {
$for field in fields_of(T) {
[field]: compute_value(field)
}
}
Pros: Natural extension of struct literal syntax.
Cons:
$forgenerating struct field initializers (not expressions) requires new grammar[field]as a computed field name in struct literals doesn’t exist yet- Struct literal syntax
T { ... }currently requires static field names - The parser would need to handle
$forinside struct literal bodies
Verdict: Appealing syntax but requires deeper parser changes than $construct<T>. Could be explored as sugar for $construct<T> in a future proposal.
5. Interaction with Other Features
5.1 With $for and fields_of
$construct<T> is designed to be the natural dual of $for field in fields_of(T) yield ...:
- Read:
$for field in fields_of(T) yield value.[field].to_str()— extract all fields - Write:
$construct<T>($for field in fields_of(T) yield (field, parse(field.name)))— build from all fields
5.2 With Error Propagation (?)
The ? operator works naturally inside the $for body:
$construct<T>(
$for field in fields_of(T) yield {
(field, parse_field(json, field.name)?) // ? propagates errors
}
)
Each expanded iteration gets its own ?. If any field fails to parse, the error propagates immediately.
5.3 With $if for Conditional Fields
$construct<T>(
$for field in fields_of(T) yield {
$if is_option(value.[field]) then {
(field, obj[field.name].map(transform: v -> FromJson.from_json(json: v)))
} else {
(field, FromJson.from_json(json: obj[field.name] ?? JsonValue.Null)?)
}
}
)
5.4 With Newtypes
For newtypes (type UserId = int), $construct<UserId> works with the single "inner" field:
$construct<UserId>(
$for field in fields_of(UserId) yield {
(field, parse_int(json)?)
}
)
// Expands to: UserId(parse_int(json)?)
5.5 With Traits and Default Impls
trait FromJson {
@from_json (json: JsonValue) -> Result<Self, JsonError>
}
pub def impl FromJson {
@from_json (json: JsonValue) -> Result<Self, JsonError> = match json {
JsonValue.Object(obj) -> {
$construct<Self>(
$for field in fields_of(Self) yield {
(field, FromJson.from_json(json: obj[field.name] ?? JsonValue.Null)?)
}
)
},
_ -> Err(JsonError { kind: TypeMismatch, path: "", message: `expected object for {name_of(Self)}` }),
}
}
6. Error Messages
E0470: Missing field in $construct
error[E0470]: $construct<User> is missing field `email`
--> src/main.ori:10:5
|
10 | $construct<User>(
| ^^^^^^^^^^^^^^^^ missing field `email` (type: Option<str>)
|
= help: ensure $for covers all fields, or use $construct_partial<User> with Default
E0471: Duplicate field in $construct
error[E0471]: $construct<User> has duplicate field `name`
--> src/main.ori:12:10
|
12 | (name_field, "duplicate"),
| ^^^^^^^^^^ `name` already provided at line 11
E0472: Field does not belong to type
error[E0472]: field `height` does not exist on type `User`
--> src/main.ori:14:10
|
14 | (height_field, 180),
| ^^^^^^^^^^^^ `User` has fields: name, age, email
E0473: $construct_partial without Default
error[E0473]: $construct_partial requires T: Default
--> src/main.ori:10:5
|
10 | $construct_partial<Config>(...)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Config` does not implement `Default`
|
= help: add `: Default` to the type declaration, or use $construct with all fields
7. Implementation Strategy
Phase 1: $construct<T> Core
Parser:
- Parse
$construct<T>(...)as a compile-time intrinsic call - The argument is a single expression (typically
$for...yield) - Add
ExprKind::Construct { type_name: Name, args: ExprId }to IR
Type Checker:
- Validate that
Tis a concrete struct type at monomorphization - Validate that
argsevaluates to a list of($FieldMeta, value)pairs - Check completeness (all fields covered), no duplicates, no extras
- Type-check each value against expected field type
- Emit
ExprKind::Struct { name, fields }after expansion
Evaluator:
- Handle
ExprKind::Constructby evaluating each field value and building a struct
LLVM Codegen:
- After monomorphization,
$constructis already expanded toExprKind::Struct - No LLVM changes needed
Compile-Time Evaluation Limits
$construct expansion is subject to the same compile-time evaluation limits as all $ operations (1M steps, 1000 recursion depth, 100MB memory, 10s time), per the const-evaluation-termination-proposal. In practice, struct construction is trivially within limits — a struct with 1000 fields produces 1000 pairs, well under the step limit.
Phase 2: $construct_partial<T: Default>
- Same as Phase 1 but allows incomplete field lists
- Missing fields filled with
Default.default()for the field type - Adds
Defaultbound requirement
8. Open Questions
Q1: Should $construct accept the list directly or via $for?
Decision: $construct<T>(expr) takes a single expression (§3.2). The function-call form is simpler and more composable. A special block syntax could be explored as sugar in a future proposal.
Q2: Should $construct work for sum types (variant construction)?
Current design: Structs only. Variant construction deferred to a future proposal (alongside $match for variant pattern matching from Q2 of the reflection proposal).
Future possibility: $construct_variant<T>(variant_meta, field_values) — builds a specific variant of a sum type.
Q3: Named fields vs positional
Current design: Fields identified by $FieldMeta (which carries the name). The compiler matches by name.
Alternative: Positional — fields matched by index. Simpler but fragile (field reordering breaks code).
Recommendation: By name (via $FieldMeta). This is robust against field reordering and aligns with Ori’s named-argument philosophy.
9. Non-Goals
- Runtime struct construction —
$constructis compile-time only. No runtime reflection needed. - Type construction from metadata — Cannot create NEW types. Only construct values of EXISTING types.
- Sum type construction — Deferred. Variant construction is a separate concern (see Q2).
- Macro-level code generation —
$constructoperates on typed values, not tokens or syntax trees. - Mutable accumulation —
$constructis a single expression, not an imperative sequence of mutations.
10. Summary
| Component | Syntax | Purpose |
|---|---|---|
$construct<T>(pairs) | Intrinsic | Build struct from compile-time field/value pairs |
$construct_partial<T>(pairs) | Intrinsic | Build struct with defaults for missing fields |
($FieldMeta, value) | Tuple | Field/value pair for construction |
Key properties:
- Zero runtime overhead (expands to direct struct literal)
- Composable with
$for+fields_of(the natural dual of field iteration) - Type-safe (each field value checked against expected type)
- Complete (all fields must be provided — or use
$construct_partialwith Default) $prefix convention (consistent with$for,$if,$FieldMeta)
Enables: Generic deserialization, ORM mapping, configuration loading, builder patterns — all with zero-cost, type-safe, compile-time struct construction.
Related Proposals
- Depends on:
approved/compile-time-reflection-proposal.md(2026-03-26) —fields_of,$for,$FieldMeta - Completes: Q1 from the reflection proposal (struct construction from
$for) - Enables:
drafts/stdlib-json-native-parser-proposal.md(pure Ori JSON deserialization) - Independent of:
approved/const-generics-proposal.md,approved/capability-unification-generics-proposal.md