Normalization, pretty-printing, and cloning

Three AST utilities accompany the parser. Each is independently useful for building a downstream OCL consumer.

Normalization

besser.BUML.notations.ocl.normalization.normalize.normalize() rewrites a parsed OCLConstraint into a canonical form: syntactic sugar (implies, xor, etc.) is reduced to a smaller core of operators (not, or, and). Targeting the canonical form lets a downstream tool define semantics for fewer node shapes.

from besser.BUML.notations.ocl.api import parse_ocl
from besser.BUML.notations.ocl.normalization.normalize import normalize

c = parse_ocl(
    "context Account inv: self.is_active implies self.balance >= 0",
    model, context_class=account,
)
c_norm = normalize(c, model)
# c_norm.ast is now an `or` of `(not self.is_active)` and the original RHS.

The driver runs each rule in a fixed order and walks the AST in post-order until no rule applies. Each rule is designed to strictly decrease a lexicographic measure ⟨#sugar-operators, AST size, #negations⟩, so the pipeline reaches a fixpoint in a bounded number of iterations. A max_iterations guard surfaces a developer bug (a rule that violates the measure) instead of looping forever.

The input is not mutated: normalize clones the AST internally before rewriting.

Pretty-printing

besser.BUML.notations.ocl.pretty_printer.pretty_print() renders an AST back into human-readable OCL source text. It is used by OCLConstraint itself to keep constraint.expression in sync with constraint.ast.

from besser.BUML.notations.ocl.pretty_printer import pretty_print

pretty_print(c.ast)            # 'self.is_active implies self.balance >= 0'
pretty_print(c)                # 'context Account inv: self.is_active implies self.balance >= 0'
pretty_print(c_norm.ast)       # 'not self.is_active or self.balance >= 0'

For a parsed-then-normalized constraint, pretty_print(c.ast) is a useful round-trip check that the AST is well-formed — the result should itself be re-parseable by parse_ocl.

Cloning

besser.BUML.metamodel.ocl.clone.clone() returns a deep copy of an AST node. Structural-model references (Properties, Classes, Types) are shared, not duplicated — only the AST scaffolding is duplicated.

from besser.BUML.metamodel.ocl import clone

fresh = clone(c.ast)
# `fresh` and `c.ast` are independent trees, but
# fresh.referredProperty is c.ast.referredProperty (same Property instance).

Use clone whenever you mutate an AST (rewriting, normalization, encoding-time substitution) rather than mutating the parsed tree in-place — leaving the parsed tree untouched lets you keep showing the user’s original source alongside any transformed view.