Class CompileTimeConstraintEvaluator

java.lang.Object
org.ek9lang.compiler.support.CompileTimeConstraintEvaluator

public final class CompileTimeConstraintEvaluator extends Object
Evaluates a ConstraintExpr against a compile-time-constant subject value, returning a tri-state verdict. Used in phase 5 to turn a guaranteed runtime constraint Panic into a compile error when the constructor argument is a literal.

This is a tiny reflective interpreter over the real EK9 standard library. It constructs the subject and each constraint literal as actual org.ek9.lang.* objects via their _of(String) factory — the same factory the bytecode generator emits (see LiteralBytecodeGenerator) — then invokes the real EK9 operator (resolved through OperatorMap, e.g. >=_gteq). Because object construction and operator evaluation reuse the exact stdlib code the runtime uses, a compile-time verdict is byte-for-byte faithful to runtime behaviour for any constrainable base type (Integer, Float, Date, Time, DateTime, Duration, Money, Dimension, Colour, Character, String/RegEx, …) — there is no per-type special-casing.

AND/OR combine the leaf verdicts with three-valued (Kleene) logic; parentheses are transparent in the model. Only the leaf comparison is reflective; the boolean algebra stays in plain Java.

Deliberately CONSERVATIVE: it returns CompileTimeConstraintEvaluator.Verdict.FAIL only when it can prove the constraint is violated. Anything it cannot fully evaluate — a type that will not load, a literal that will not construct (or constructs to an unset value), an operator with no resolvable method, an operator result that is unset, or any reflective failure — yields CompileTimeConstraintEvaluator.Verdict.UNKNOWN, and the caller must NOT emit an error (the runtime check still applies). Crucially, because subject and literal are built with the same _of the runtime uses, the evaluator can never compute a verdict that diverges from runtime, so it can never raise a false-positive compile error on valid code.

  • Constructor Details

    • CompileTimeConstraintEvaluator

      public CompileTimeConstraintEvaluator()
  • Method Details

    • evaluate

      public CompileTimeConstraintEvaluator.Verdict evaluate(String subjectText, String subjectTypeFqn, ConstraintExpr constraint)
      Evaluate the constraint against a constant subject.
      Parameters:
      subjectText - the literal source text of the constructor argument (e.g. "15")
      subjectTypeFqn - the argument's fully qualified type (e.g. "org.ek9.lang::Integer")
      constraint - the constraint model (may be null)
      Returns:
      the verdict; only FAIL should trigger a compile error
    • evaluate

      public CompileTimeConstraintEvaluator.Verdict evaluate(String subjectText, String subjectTypeFqn, String promoteToTypeFqn, ConstraintExpr constraint)
      As above, but first applies the type's #^ PROMOTION when the argument's own type is not the constrained base type.

      🔑 Without this the subject is built as its OWN type and compared against a constraint written in the BASE type, so the reflective comparison never resolves and every such site fell through to a runtime Panic. Ratio(2) for Ratio as Float constrain as >= 0.0 and <= 1.0 compiled cleanly and Panicked, while the identical Ratio(2.0) was correctly rejected — the same violation caught or missed purely by how the literal was spelled.

      Parameters:
      promoteToTypeFqn - the type to promote the built subject to before evaluating, or null for none. A type declares at most ONE promotion, so this is a single, unambiguous step; the caller establishes it via TypeCoercions.promotionTarget, which is THE accessor for it.
    • steppedLiteral

      public String steppedLiteral(String text, String typeFqn, boolean increment)
      The literal text of text's immediate neighbour, obtained by invoking the type's OWN ++ / -- operator, or null when the type declares none or the result will not round-trip.

      🔑 Why this lives here. Stepping a value needs exactly the machinery this class already owns — build via _of(String), resolve an operator through OperatorMap, invoke it reflectively — and a second copy elsewhere would be free to drift from the evaluator about what a literal means. It is also what keeps the caller type-AGNOSTIC: a Date neighbour is the next day and a Character neighbour is the next character because those types say so, not because anything here knows about calendars or code points.

      The result is rendered back to text with $ (_string) so it can be re-probed and emitted as source. A type whose $ does not round-trip through its _of simply yields a candidate that fails the next evaluation and is dropped — callers must verify anyway, so an imperfect round-trip costs a wasted probe and never a wrong value.

      Parameters:
      text - the literal source text to step from
      typeFqn - the value's fully qualified type
      increment - true for ++, false for --
      Returns:
      the neighbour's literal text, or null when it cannot be produced