Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,13 @@ export class ModelTransformationLabelEditValidator extends BaseLabelEditValidato
/**
* Checks whether an operator is valid for the given modifier kind.
*
* Only NONE (no modifier) and CREATE allow assignments (`=`).
* All other modifiers (DELETE, FORBID, REQUIRE) must use comparison (`==`).
* Only NONE (no modifier) and CREATE allow the assignment operator (`=`).
* All comparison operators (`==`, `!=`, `<`, `>`, `<=`, `>=`) are permitted
* for every modifier kind.
* PatternObjectInstanceReference nodes (which have no modifier field) also
* only allow comparisons.
* only allow comparison operators.
*
* @param operator The operator string (`=` or `==`)
* @param operator The operator string (`=`, `==`, `!=`, `<`, `>`, `<=`, or `>=`)
* @param modifier The modifier kind of the enclosing instance, or 'reference' for references
* @returns A validation status if the operator is not permitted, undefined otherwise
*/
Expand All @@ -373,7 +374,7 @@ export class ModelTransformationLabelEditValidator extends BaseLabelEditValidato
if (modifier === "reference") {
if (operator === "=") {
return this.error(
"Assignment ('=') is not allowed in an instance reference. Use '==' for comparisons."
"Assignment ('=') is not allowed in an instance reference. Use a comparison operator (==, !=, <, >, <=, >=)."
);
}
return undefined;
Expand All @@ -384,7 +385,7 @@ export class ModelTransformationLabelEditValidator extends BaseLabelEditValidato
if (!allowed) {
return this.error(
`Assignment ('=') is not allowed with modifier '${modifier ?? "none"}'. ` +
`Only 'none' (no modifier) and 'create' instances support assignment. Use '==' for comparisons.`
`Only 'none' (no modifier) and 'create' instances support assignment. Use a comparison operator (==, !=, <, >, <=, >=).`
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Used by both the apply-label-edit handler and the label-edit validator.
*/

import type { PatternPropertyOperator } from "../../plugin/typedAst.js";

/**
* Parses an instance label in `name : type` format.
*
Expand All @@ -21,14 +23,20 @@ export function parseInstanceLabel(label: string): { name: string; type: string

/**
* Finds the index of the assignment `=` operator in a string,
* skipping over `==` comparison operators.
* skipping over multi-character operators (`==`, `!=`, `<=`, `>=`).
*
* @param text The text to search
* @returns The index of the single `=`, or -1 if not found
*/
export function findAssignmentIndex(text: string): number {
for (let i = 0; i < text.length; i++) {
if (text[i] === "=") {
const ch = text[i];
// Skip `!=`, `<=`, `>=` — the `=` here belongs to the multi-char operator.
if ((ch === "!" || ch === "<" || ch === ">") && text[i + 1] === "=") {
i++; // skip the `=` that follows
continue;
}
if (ch === "=") {
// Skip `==`.
if (text[i + 1] === "=") {
i++;
Expand Down Expand Up @@ -65,38 +73,61 @@ export function parseVariableLabel(label: string): { name: string; value: string
return name.length > 0 && value.length > 0 ? { name, value } : undefined;
}

/**
* All multi-character comparison operators, checked longest-first so that
* `<=` is found before a bare `<`, etc.
*/
const MULTI_CHAR_OPERATORS: PatternPropertyOperator[] = ["==", "!=", "<=", ">="];

/**
* All single-character operators.
*/
const SINGLE_CHAR_OPERATORS: PatternPropertyOperator[] = ["<", ">", "="];

/**
* Parses a property assignment label into its three parts.
*
* Supports both assignment (`=`) and comparison (`==`) operators.
* The first `=` occurrence determines operator type; `==` takes precedence.
* Supports all property operators: `=`, `==`, `!=`, `<`, `>`, `<=`, `>=`.
* Multi-character operators are checked before single-character ones to avoid
* prefix mismatches (e.g. `<=` found before `<`).
*
* @param label The raw label text to parse
* @returns The parsed `{ name, operator, value }`, or an error message string if parsing fails
*/
export function parseModelTransformationPropertyLabel(
label: string
): { name: string; operator: "=" | "=="; value: string } | string {
const eqIndex = label.indexOf("=");
if (eqIndex === -1) {
return "Missing '=' or '==' in property label.";
): { name: string; operator: PatternPropertyOperator; value: string } | string {
// Try multi-character operators first (longest-match).
for (const op of MULTI_CHAR_OPERATORS) {
const idx = label.indexOf(op);
if (idx === -1) continue;
const name = label.substring(0, idx).trim();
const value = label.substring(idx + op.length).trim();
if (name.length === 0) {
return "Missing property name before operator.";
}
if (value.length === 0) {
return "Missing value expression after operator.";
}
return { name, operator: op, value };
}

const isDouble = label[eqIndex + 1] === "=";
const operator: "=" | "==" = isDouble ? "==" : "=";
const operatorEnd = eqIndex + operator.length;

const name = label.substring(0, eqIndex).trim();
const value = label.substring(operatorEnd).trim();

if (name.length === 0) {
return "Missing property name before operator.";
}
if (value.length === 0) {
return "Missing value expression after operator.";
// Fall back to single-character operators.
for (const op of SINGLE_CHAR_OPERATORS) {
const idx = label.indexOf(op);
if (idx === -1) continue;
const name = label.substring(0, idx).trim();
const value = label.substring(idx + op.length).trim();
if (name.length === 0) {
return "Missing property name before operator.";
}
if (value.length === 0) {
return "Missing value expression after operator.";
}
return { name, operator: op, value };
}

return { name, operator, value };
return "Missing operator in property label.";
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,25 @@ export function generateModelTransformationRules(): {

/**
* Pattern property assignment rule.
* Format: property = value or property == value
* Format: property op value, where op is one of: =, ==, !=, <, >, <=, >=
* The assignment operator (=) sets a property value; comparison operators
* (==, !=, <, >, <=, >=) constrain matching. Multi-character operators are
* listed before single-character ones to avoid prefix mismatches.
* Uses expression for value to support complex expressions.
*/
const PatternPropertyAssignmentRule = createRule("PatternPropertyAssignmentRule")
.returns(PatternPropertyAssignment)
.as(({ set }) => [
set("name", ref(Property, ID)),
or(set("operator", "=="), set("operator", "=")),
or(
set("operator", "=="),
set("operator", "!="),
set("operator", "<="),
set("operator", ">="),
set("operator", "<"),
set("operator", ">"),
set("operator", "=")
),
set("value", ExpressionRule)
]);

Expand Down
17 changes: 15 additions & 2 deletions app/packages/language-model-transformation/src/plugin/typedAst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ export interface TypedPatternVariable {
value: TypedExpression;
}

/**
* Discriminated union of all operators that can appear in a
* PatternPropertyAssignment.
*
* - `"="` — assignment (sets the property when creating/modifying an instance).
* - `"=="` — equality comparison constraint.
* - `"!="` — inequality comparison constraint.
* - `"<"`, `">"`, `"<="`, `">="` — relational comparison constraints
* (only valid for comparable / numeric property types).
*/
export type PatternPropertyOperator = "=" | "==" | "!=" | "<" | ">" | "<=" | ">=";

/**
* Property assignment in a pattern object instance.
*/
Expand All @@ -64,9 +76,10 @@ export interface TypedPatternPropertyAssignment {
propertyName: string;

/**
* The operator used (= for assignment, == for comparison).
* The operator used.
* `"="` is for assignment; all others are comparison constraints.
*/
operator: string;
operator: PatternPropertyOperator;

/**
* The value expression.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ import {
PatternObjectInstanceDelete,
type PatternObjectInstanceDeleteType,
type PatternObjectInstanceReferenceType,
PatternObjectInstanceReference
PatternObjectInstanceReference,
type PatternPropertyOperator
} from "@mdeo/language-model-transformation";
import { AssociationEnd, type AssociationEndType } from "@mdeo/language-metamodel";
import type { AstNode } from "langium";
Expand Down Expand Up @@ -361,7 +362,7 @@ export class ModelTransformationTypedAstConverter extends TypedAstConverter {
className: obj.class?.$refText ?? "",
properties: obj.properties.map((prop) => ({
propertyName: prop.name?.$refText ?? "",
operator: prop.operator,
operator: prop.operator as PatternPropertyOperator,
value: this.convertExpression(prop.value)
}))
};
Expand All @@ -380,7 +381,7 @@ export class ModelTransformationTypedAstConverter extends TypedAstConverter {
name: objRef.instance.$refText,
properties: objRef.properties.map((prop) => ({
propertyName: prop.name?.$refText ?? "",
operator: prop.operator,
operator: prop.operator as PatternPropertyOperator,
value: this.convertExpression(prop.value)
}))
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ data class TypedPatternVariable(
* (matching against a property value) within a pattern.
*
* @param propertyName Name of the property being assigned or compared.
* @param operator The operator used: "=" for assignment, "==" for comparison.
* @param operator The operator used:
* - `"="` — assignment (sets the property value).
* - `"=="` — equality comparison constraint.
* - `"!="` — inequality comparison constraint.
* - `"<"`, `">"`, `"<="`, `">="` — relational comparison constraints
* (only valid for comparable / numeric property types).
* @param value The value expression for the assignment or comparison.
*/
@Serializable
Expand Down
Loading
Loading