Skip to content

Bind struct field constraints to constructor applications instead of per-field lookups #1944

Closed

Description

@shunichironomura

Warning

This content was written by an AI agent and must be verified by a human developer. After human verification, this alert may be removed.

Summary

Struct field constraints (min:/max: on fields) are looked up per field at check and evaluation time. check_const_struct_field_constraints silently skips a field when no constraint is found, so "this field has no constraint" and "the constraint lookup missed" are indistinguishable. There is no known repro, but the gap means a missed lookup would silently skip validation.

Current state

  • graphcal-eval: check_const_struct_field_constraints, the field-constraint lookup in eval_expr/hir_eval.rs, and the one in execution_check/domain_resolve.rs.
  • Field-constraint bounds can reference evaluated constants and Nat bindings, so they are resolved in execution_check after constants are evaluated. They cannot be attached to the constructor node at check time.

Proposed direction

Build a per-DAG table of constructor applications indexed by a slot interned by the compiler (similar to CallTargets / CallSlot), rehomed through specialization. execution_check then fills resolved constraints per slot, and evaluation reads them without a name lookup. "No constraint" becomes an explicit value instead of a missing entry.

Notes

Making the skip an internal error without this redesign would add an invariant site that could fire spuriously.

Activity

  1. shunichironomura commented on Oct 7, 2026

    @shunichironomura
    MemberAuthor

    Warning

    This content was written by an AI agent and must be verified by a human developer. After human verification, this alert may be removed.

    Found a reproducible constructor field-constraint bypass related to this issue, although it does not demonstrate the missing-lookup scenario described in the summary.

    Minimal reproducer

    Save as repro.gcl:

    type Spec { Spec(mass: Mass(max: 2000.0 kg)) }
    const node BAD: Mass = Spec(mass: 5000.0 kg).mass;
    

    Run:

    graphcal check repro.gcl
    graphcal eval repro.gcl

    Actual: both commands exit successfully; evaluation prints:

    BAD = 5000 kg
    

    Expected: reject the constructor application because Spec.mass exceeds its maximum of 2000.0 kg.

    Controls

    • Changing const node BAD to node bad correctly fails at evaluation with field Spec.mass above maximum (2000 kg).
    • Keeping the struct as the constant's final value (const node BAD: Spec = Spec(mass: 5000.0 kg);) correctly produces a compile-time domain violation.

    Cause

    Constant evaluation runs with a provisional session before field constraints are resolved. eval_constructor_call in crates/graphcal-eval/src/eval_expr/hir_eval.rs skips constructor field validation when that session has no field-constraint table. Once constraints are resolved, check_const_struct_field_constraints in crates/graphcal-eval/src/execution_check/domain_resolve.rs recursively validates only the final constant values.

    In the reproducer, field projection discards the temporary struct and leaves a scalar Mass value, so the later recursive check never sees the invalid constructor application. This is a validation-coverage gap rather than a missed constraint key; binding constraints to constructor applications would also need to ensure applications evaluated during the constant phase are eventually validated.

    Verified locally against a fresh cargo build -p graphcal build.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingrefactor

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions