OCL Constraints
The Object Constraint Language (OCL) is the OMG-standard expression language for adding
invariants, pre-conditions, and post-conditions to UML models. kUML implements the full
OCL 2.4/2.5 expression language — collection iterators, let/if, type operations,
association navigation, and more — for invariants, pre/post-conditions, and state-machine
guards. A short list of deliberately out-of-scope features remains; see
Deliberately not supported.
Invariants on classifiers
Add constraint(name, body) inside any classifier:
classOf(name = "Order") {
attribute(name = "total", type = "BigDecimal")
attribute(name = "discount", type = "BigDecimal")
constraint(name = "NonNegativeTotal",
body = "self.total >= 0")
constraint(name = "DiscountWithinLimits",
body = "self.discount >= 0 and self.discount <= self.total")
}
The body is plain OCL. self always refers to the surrounding classifier instance.
Validation
kuml validate order.kuml.kts
The CLI parses every constraint, evaluates it, and exits with code:
-
0if every constraint holds. -
1if any constraint is violated. Each violation prints the constraint name, the classifier it sits on, and the OCL expression. -
non-zero if a constraint fails to parse (syntax error).
In a Gradle build, the kumlValidate task runs the same check and fails the build if
failOnValidationViolations is true (the default).
Supported OCL language (v0.23.0)
| Category | Supported |
|---|---|
Boolean operators |
|
Comparison |
|
Arithmetic |
|
Literals |
Integer ( |
Navigation |
|
Operation calls |
|
|
|
|
|
Collection ops |
|
Iterators |
|
Conditionals |
|
Type operations |
|
Pre-state |
|
State-machine guards
Transition guards on state machines use the same OCL expression language, with two additional
implicit variables: event (the triggering event with its payload as a Map<String, Any?>)
and the model instance binding.
stateMachine(name = "Cart") {
initial("empty")
state("active") {
// Read attribute from current variables map
constraint(name = "PositiveItemCount", body = "self.itemCount > 0")
}
state("checked_out") { final() }
transition(from = "active", to = "checked_out",
trigger = "checkout",
guard = "event.totalCents > 0 and self.itemCount > 0")
}
The runtime parses the guard at evaluation time, navigates event.totalCents against the
event’s payload map, and self.itemCount against the machine’s variables map. Both are
populated by the simulator’s event stream — see simulate.
Guard expressions (on state machine transitions, activity/BPMN decision and gateway
edges alike) additionally accept the C-like operators !, !=, &&, || alongside the
OCL keywords not, <>, and, or from the table above — !allow and not allow are
both valid and equivalent for a bare identifier. Both spellings bind identically in longer
expressions too: ! and not both bind looser than comparison, so !a == b (C-like)
and not a = b (OCL) each negate the whole comparison — !(a == b) / not (a = b), not
(!a) == b. Still, pick one dialect per expression rather than mixing ! and not (or
&&/and, ||/or, !=/<>) in the same guard — each front-end only recognizes its
own operator tokens.
|
Guards are fail-closed with respect to variables that were never provided. A
comparison against a variable that is missing from event’s payload or from the model
instance’s variables map — `event.status <> 'rejected', vars.approved != true, or the
bare-identifier equivalents — never evaluates to a trusted true just because a missing
lookup resolves to null the same way a present-but-null value would. Instead the guard
evaluates to GuardResult.Failed (the transition or edge does not fire), so a guard can
never silently take the "allowed" branch on data the event stream never sent. This
fail-closed result applies uniformly to state machine transitions and to activity/BPMN
decision and gateway edges alike — but how the failure is surfaced for diagnosis differs
by runtime path. On state machine transitions (StateMachineRuntime) it is recorded as a
TraceEntry.GuardWarning in the trace. On activity/BPMN decision and gateway edges
(ActivityRuntime, TokenFlowEngine) no trace entry is written — ActivityRuntime simply
skips the edge with no diagnostic at all, and TokenFlowEngine only reports the failure
through its optional guardResultListener callback, which is a no-op unless a caller wires
it up; the kuml simulate CLI does, printing Warning [GUARD_EVALUATION_FAILED]: … to
stderr.
A function call anywhere in a guard expression (audit() != 1) is treated the same way,
since call results are not resolved by the expression evaluator. &&/||/and/or remain
short-circuiting: isVip || spendOver1000 still fires on isVip = true even when
spendOver1000 was never set, because the right-hand side is never consulted.
|
vars.x <> null and vars.x != null are unaffected by the fail-closed rule above and
remain a valid "is X set?" idiom: a missing x makes the comparison evaluate to false
(not true), so it is never a candidate for the fail-closed downgrade in the first place —
the transition or edge simply does not fire, exactly as when x is present and null.
Because of that, <> null/!= null cannot distinguish "missing" from "present but null`"
— both read as `false. oclIsUndefined() does not draw that line either: it evaluates to
receiver == null, so it is true both for a genuinely missing variable and for one that
is present with an explicit null value — not vars.x.oclIsUndefined() is therefore exactly
equivalent to vars.x <> null, not a way around its blind spot. This OCL subset has no
operator that tells "never set" apart from "set to null`"; a guard that must make that
distinction needs the model to carry an explicit sentinel (e.g. a separate boolean variable
such as `vars.cancelReasonProvided) rather than relying on the value of x alone.
The mirror-image idiom is not symmetric. vars.x = null, vars.x == null,
and not (vars.x <> null) — the "is X not set?" spelling — are exactly the shape the
fail-closed rule above targets: a missing x makes the comparison evaluate to a true
that is built on data that was never provided, so it is downgraded from GuardResult.True
to GuardResult.Failed (transition/edge does not fire, instead of firing on a silent
true) — surfaced as a GuardWarning trace entry on the state machine path, or through
the diagnostic channel described above on the activity/BPMN path. This is a behavior
change from before this rule existed, where a missing x made vars.x = null evaluate to
a trusted true. A guard
written to mean "fire when X has never been set" must use vars.x.oclIsUndefined()
instead — it evaluates to true for a genuinely missing variable without going through a
null-tolerant comparison at all.
|
Profile-level constraints
Stereotypes can carry constraints that apply automatically to any element with that stereotype:
profile(name = "JavaEE") {
stereotype(name = "Entity", extending = UmlMetaclass.Class) {
constraint(name = "RequiresIdAttribute",
body = "self.ownedAttribute->exists(a | a.appliedStereotype.name = 'Id')")
}
}
When you applyProfile("JavaEE") and stereotype a class as Entity, the validator
checks the constraint without you adding it manually.
Deliberately not supported
-
Collection-literal syntax (
Set{1, 2, 3},Bag{…}) — collections are only produced via navigation orcollect, never literal construction -
Tuple types and tuple literals
-
oclType()reflection -
Message expressions (
^,^^) -
Real invocation semantics for user-defined model operations — call syntax (
self.someOperation(1, 2)) parses, but there is no operation-body execution runtime behind it (no return-value computation beyond standard-library ops)
If you hit a limit, fall back to a smaller constraint or split it into multiple named constraints — the validator runs them independently anyway.
Conformance
kuml-core-ocl’s parser and evaluator are exercised against representative example
expressions manually transcribed from the public OMG Object Constraint Language
(OCL), Version 2.4 specification (formal/2014-02-03) — see
`kuml-core/kuml-core-ocl/src/test/kotlin/dev/kuml/core/ocl/OclConformanceTest.kt for
the full suite and a maintained feature-coverage matrix. There is no downloadable,
machine-readable OMG OCL conformance test-case package publicly available, so this is
a best-effort check against the spec’s published examples — not an official OMG
certification. Evaluation-time regressions are anchored by
OclBenchmarkTest.kt against small and 10,000-element collections.