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:

  1. Deserialization — JSON/TOML/YAML text to typed struct
  2. ORM mapping — database row to struct
  3. Configuration loading — env vars / config file to struct
  4. Builder patterns — accumulate fields, produce struct
  5. Default-with-overridesT from defaults plus partial input
  6. 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:

  1. Evaluates the field list (the $for expansion produces N pairs)
  2. Matches each $FieldMeta to the corresponding struct field by name
  3. Type-checks each value against the expected field type
  4. 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. $construct is designed for generic contexts where T is 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:

  • field must be a $FieldMeta from fields_of(T) (verified at compile time)
  • value must 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 T has exactly one (field, value) pair
  • No duplicates: no field appears twice
  • No extras: no $FieldMeta that doesn’t belong to T

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 T has 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 writing T { ... } 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: Default even when all fields are provided
  • Each result.[field] = x desugars to result = { ...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] = value inside $build has different semantics than outside (accumulation vs assignment)
  • Harder to compose with $for...yield (the do form 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:

  • $for generating 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 $for inside 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 T is a concrete struct type at monomorphization
  • Validate that args evaluates 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::Construct by evaluating each field value and building a struct

LLVM Codegen:

  • After monomorphization, $construct is already expanded to ExprKind::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 Default bound 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

  1. Runtime struct construction$construct is compile-time only. No runtime reflection needed.
  2. Type construction from metadata — Cannot create NEW types. Only construct values of EXISTING types.
  3. Sum type construction — Deferred. Variant construction is a separate concern (see Q2).
  4. Macro-level code generation$construct operates on typed values, not tokens or syntax trees.
  5. Mutable accumulation$construct is a single expression, not an imperative sequence of mutations.

10. Summary

ComponentSyntaxPurpose
$construct<T>(pairs)IntrinsicBuild struct from compile-time field/value pairs
$construct_partial<T>(pairs)IntrinsicBuild struct with defaults for missing fields
($FieldMeta, value)TupleField/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_partial with 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.


  • 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