GuardExpression.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Carsten Hammer - initial API and implementation
 *******************************************************************************/
package org.sandbox.jdt.triggerpattern.api;

import java.util.List;
import java.util.Objects;
import java.util.function.Function;

/**
 * AST model for guard expressions using a sealed interface pattern.
 * 
 * <p>Guard expressions are used to constrain pattern matches. They support
 * function calls, logical operators ({@code &&}, {@code ||}, {@code !}),
 * and parenthesized sub-expressions.</p>
 * 
 * <p>Examples:</p>
 * <ul>
 *   <li>{@code sourceVersionGE(11)}</li>
 *   <li>{@code $x instanceof String}</li>
 *   <li>{@code $x instanceof String && sourceVersionGE(11)}</li>
 *   <li>{@code !isStatic($x)}</li>
 * </ul>
 * 
 * @since 1.3.2
 */
public sealed interface GuardExpression
		permits GuardExpression.FunctionCall, GuardExpression.And, GuardExpression.Or, GuardExpression.Not {

	/** Semantic truth state independent from optional compatibility fallback. */
	enum TruthValue {
		MATCH,
		NO_MATCH,
		UNKNOWN
	}

	/**
	 * Detailed guard result.
	 *
	 * @param truthValue semantic three-valued result
	 * @param compatibilityValue historical boolean result used by optional hints
	 */
	record Evaluation(TruthValue truthValue, boolean compatibilityValue) {
	}
	
	/**
	 * Sets the guard function resolver used by {@link FunctionCall} to look up
	 * guard functions by name.
	 * 
	 * @param resolver a function that maps guard function names to implementations
	 */
	static void setGuardFunctionResolver(Function<String, GuardFunction> resolver) {
		GuardFunctionResolverHolder.setResolver(resolver);
	}
	
	/**
	 * Returns the current guard function resolver.
	 * 
	 * @return the resolver, or {@code null} if not set
	 */
	static Function<String, GuardFunction> getGuardFunctionResolver() {
		return GuardFunctionResolverHolder.getResolver();
	}
	
	/**
	 * Evaluates this guard expression against the given context.
	 * 
	 * @param ctx the guard context
	 * @return {@code true} if the guard condition is satisfied
	 */
	boolean evaluate(GuardContext ctx);

	/**
	 * Evaluates this expression with three-valued semantic tracking while
	 * retaining the historical boolean fallback for optional hints.
	 */
	default Evaluation evaluateDetailed(GuardContext ctx) {
		return switch (this) {
		case FunctionCall call -> evaluateFunction(call, ctx);
		case And and -> evaluateAnd(and, ctx);
		case Or or -> evaluateOr(or, ctx);
		case Not not -> evaluateNot(not, ctx);
		};
	}

	private static Evaluation evaluateFunction(FunctionCall call, GuardContext ctx) {
		int unknownBefore= ctx.unknownSemanticRequirementCount();
		boolean compatibility= call.evaluate(ctx);
		if (ctx.unknownSemanticRequirementCount() > unknownBefore) {
			return new Evaluation(TruthValue.UNKNOWN, compatibility);
		}
		return new Evaluation(compatibility ? TruthValue.MATCH : TruthValue.NO_MATCH,
				compatibility);
	}

	private static Evaluation evaluateAnd(And and, GuardContext ctx) {
		Evaluation left= and.left().evaluateDetailed(ctx);
		if (left.truthValue() == TruthValue.NO_MATCH) {
			return new Evaluation(TruthValue.NO_MATCH, false);
		}
		Evaluation right= and.right().evaluateDetailed(ctx);
		TruthValue truth= switch (left.truthValue()) {
		case MATCH -> right.truthValue();
		case UNKNOWN -> right.truthValue() == TruthValue.NO_MATCH
				? TruthValue.NO_MATCH : TruthValue.UNKNOWN;
		case NO_MATCH -> TruthValue.NO_MATCH;
		};
		return new Evaluation(truth,
				left.compatibilityValue() && right.compatibilityValue());
	}

	private static Evaluation evaluateOr(Or or, GuardContext ctx) {
		Evaluation left= or.left().evaluateDetailed(ctx);
		if (left.truthValue() == TruthValue.MATCH) {
			return new Evaluation(TruthValue.MATCH, true);
		}
		Evaluation right= or.right().evaluateDetailed(ctx);
		TruthValue truth= switch (left.truthValue()) {
		case NO_MATCH -> right.truthValue();
		case UNKNOWN -> right.truthValue() == TruthValue.MATCH
				? TruthValue.MATCH : TruthValue.UNKNOWN;
		case MATCH -> TruthValue.MATCH;
		};
		return new Evaluation(truth,
				left.compatibilityValue() || right.compatibilityValue());
	}

	private static Evaluation evaluateNot(Not not, GuardContext ctx) {
		Evaluation operand= not.operand().evaluateDetailed(ctx);
		TruthValue truth= switch (operand.truthValue()) {
		case MATCH -> TruthValue.NO_MATCH;
		case NO_MATCH -> TruthValue.MATCH;
		case UNKNOWN -> TruthValue.UNKNOWN;
		};
		return new Evaluation(truth, !operand.compatibilityValue());
	}
	
	/**
	 * A function call guard expression (e.g., {@code sourceVersionGE(11)},
	 * {@code $x instanceof String}).
	 * 
	 * <p>The {@code instanceof} expression is modeled as a function call with the name
	 * {@code "instanceof"}, the placeholder name as the first argument, and the type
	 * name as the second argument.</p>
	 * 
	 * @param name the function name
	 * @param args the function arguments
	 */
	record FunctionCall(String name, List<String> args) implements GuardExpression {
		
		/**
		 * Creates a function call guard expression.
		 * 
		 * @param name the function name
		 * @param args the function arguments
		 */
		public FunctionCall {
			Objects.requireNonNull(name, "Function name cannot be null"); //$NON-NLS-1$
			args = List.copyOf(args);
		}
		
		@Override
		public boolean evaluate(GuardContext ctx) {
			Function<String, GuardFunction> resolver = GuardFunctionResolverHolder.getResolver();
			GuardFunction fn = resolver != null ? resolver.apply(name) : null;
			if (fn == null) {
				throw new IllegalStateException("Unknown guard function: " + name); //$NON-NLS-1$
			}
			return fn.evaluate(ctx, args.toArray());
		}
	}
	
	/**
	 * Logical AND of two guard expressions.
	 * 
	 * @param left the left operand
	 * @param right the right operand
	 */
	record And(GuardExpression left, GuardExpression right) implements GuardExpression {
		
		/**
		 * Creates a logical AND guard expression.
		 * 
		 * @param left the left operand
		 * @param right the right operand
		 */
		public And {
			Objects.requireNonNull(left, "Left operand cannot be null"); //$NON-NLS-1$
			Objects.requireNonNull(right, "Right operand cannot be null"); //$NON-NLS-1$
		}
		
		@Override
		public boolean evaluate(GuardContext ctx) {
			return left.evaluate(ctx) && right.evaluate(ctx);
		}
	}
	
	/**
	 * Logical OR of two guard expressions.
	 * 
	 * @param left the left operand
	 * @param right the right operand
	 */
	record Or(GuardExpression left, GuardExpression right) implements GuardExpression {
		
		/**
		 * Creates a logical OR guard expression.
		 * 
		 * @param left the left operand
		 * @param right the right operand
		 */
		public Or {
			Objects.requireNonNull(left, "Left operand cannot be null"); //$NON-NLS-1$
			Objects.requireNonNull(right, "Right operand cannot be null"); //$NON-NLS-1$
		}
		
		@Override
		public boolean evaluate(GuardContext ctx) {
			return left.evaluate(ctx) || right.evaluate(ctx);
		}
	}
	
	/**
	 * Logical NOT of a guard expression.
	 * 
	 * @param operand the operand to negate
	 */
	record Not(GuardExpression operand) implements GuardExpression {
		
		/**
		 * Creates a logical NOT guard expression.
		 * 
		 * @param operand the operand
		 */
		public Not {
			Objects.requireNonNull(operand, "Operand cannot be null"); //$NON-NLS-1$
		}
		
		@Override
		public boolean evaluate(GuardContext ctx) {
			return !operand.evaluate(ctx);
		}
	}
}