All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.jenetics.ext.rewriting.TreePattern Maven / Gradle / Ivy

The newest version!
/*
 * Java Genetic Algorithm Library (jenetics-8.1.0).
 * Copyright (c) 2007-2024 Franz Wilhelmstötter
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Author:
 *    Franz Wilhelmstötter ([email protected])
 */
package io.jenetics.ext.rewriting;

import static java.lang.String.format;
import static java.util.stream.Collectors.toMap;
import static io.jenetics.ext.internal.util.Names.isIdentifier;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Function;

import io.jenetics.ext.internal.util.Escaper;
import io.jenetics.ext.util.Tree;
import io.jenetics.ext.util.Tree.Path;
import io.jenetics.ext.util.TreeNode;

/**
 * This class serves two purposes. Firstly, it is used as a classical
 * pattern, which is used to find matches against a matching
 * tree. Secondly, it can expand a given pattern to a full tree with a
 * given pattern variable to subtree mapping.
 *
 * 

Matching trees

* * A compiled representation of a tree pattern. A tree pattern, * specified as a parentheses string, must first be compiled into an instance of * this class. The resulting pattern can then be used to create a * {@link TreeMatcher} object that can match arbitrary trees against the tree * pattern. All the states involved in performing a match reside in the * matcher, so many matchers can share the same pattern. *

* The string representation of a tree pattern is a parenthesis tree string, * with a special wildcard syntax for arbitrary subtrees. The subtree * variables are prefixed with a '$' and must be a valid Java identifier. * {@snippet lang="java": * final TreePattern p1 = TreePattern.compile("add($a,add($b,sin(x)))"); * final TreePattern p2 = TreePattern.compile("pow($x,$y)"); * } * * If you need to have values which start with a '$' character, you can escape * it with a '\'. * {@snippet lang="java": * final TreePattern p1 = TreePattern.compile("concat($x,\\$foo)"); * } * * The second value, {@code $foo}, of the {@code concat} function is not treated * as pattern variable. *

* If you want to match against trees with a different value type than * {@code String}, you have to specify an additional type mapper function when * compiling the pattern string. * {@snippet lang="java": * final TreePattern> p = TreePattern.compile( * "add($a,add($b,sin(x)))", * MathOp::toMathOp * ); * } * *

Expanding trees

* * The second functionality of the tree pattern is to expand a pattern to a whole * tree with a given pattern variable to subtree mapping. * {@snippet lang="java": * final TreePattern pattern = TreePattern.compile("add($x,$y,1)"); * final Map, Tree> vars = Map.of( * Var.of("x"), TreeNode.parse("sin(x)"), * Var.of("y"), TreeNode.parse("sin(y)") * ); * * final Tree tree = pattern.expand(vars); * assertEquals(tree.toParenthesesString(), "add(sin(x),sin(y),1)"); * } * * @see TreeRewriteRule * @see Tree#toParenthesesString() * @see TreeMatcher * * @param the value type of the tree than can match by this pattern * * @author Franz Wilhelmstötter * @version 7.0 * @since 5.0 */ public final class TreePattern implements Serializable { @Serial private static final long serialVersionUID = 1L; // Primary state of the tree pattern. private final TreeNode> _pattern; // Cached variable set. private final SortedSet> _vars; /** * Create a new tree-pattern object from the given pattern tree. * * @param pattern the pattern-tree * @throws NullPointerException if the given {@code pattern} is {@code null} * @throws IllegalArgumentException if {@link Var} nodes are not leaf nodes; * {@link Tree#isLeaf()} is {@code false} */ public TreePattern(final Tree, ?> pattern) { _pattern = TreeNode.ofTree(pattern); _vars = extractVars(_pattern); } // Extracts the variables from the pattern. private static SortedSet> extractVars(final TreeNode> pattern) { final SortedSet> variables = new TreeSet<>(); for (Tree, ?> n : pattern) { if (n.value() instanceof Var var) { if (!n.isLeaf()) { throw new IllegalArgumentException(format( "Variable node '%s' is not a leaf: %s", n.value(), n.toParenthesesString() )); } variables.add(var); } } return Collections.unmodifiableSortedSet(variables); } TreeNode> pattern() { return _pattern; } /** * Return the unmodifiable set of variables, defined in {@code this} * pattern. The variables are returned without the angle brackets. * * @return the variables, defined in this pattern */ public SortedSet> vars() { return _vars; } /** * Maps {@code this} tree-pattern from type {@code V} to type {@code B}. * * @param mapper the type mapper * @param the target type * @return a new tree-pattern for the mapped type */ public TreePattern map(final Function mapper) { return new TreePattern<>(_pattern.map(d -> d.map(mapper))); } /** * Creates a matcher that will match the given input tree against * {@code this} pattern. * * @param tree the tree to be matched * @return a new matcher for {@code this} pattern * @throws NullPointerException if the arguments is {@code null} */ public TreeMatcher matcher(final Tree tree) { return TreeMatcher.of(this, tree); } /** * Try to match the given {@code tree} against {@code this} pattern. * * @param tree the tree to be matched * @return the match result, or {@link Optional#empty()} if the given * {@code tree} doesn't match * @throws NullPointerException if the arguments is {@code null} */ public Optional> match(final Tree tree) { final Map, Tree> vars = new HashMap<>(); final boolean matches = matches(tree, _pattern, vars); return matches ? Optional.of(TreeMatchResult.of(tree, vars)) : Optional.empty(); } /** * Tests whether the given input {@code tree} matches {@code this} pattern. * * @param tree the tree to be matched * @return {@code true} if the {@code tree} matches {@code this} pattern, * {@code false} otherwise * @throws NullPointerException if one of the arguments is {@code null} */ public boolean matches(final Tree tree) { return matches(tree, _pattern, new HashMap<>()); } private static boolean matches( final Tree node, final Tree, ?> pattern, final Map, Tree> vars ) { final Decl decl = pattern.value(); if (decl instanceof Var var) { final Tree tree = vars.get(decl); if (tree == null) { vars.put(var, node); return true; } return tree.equals(node); } else { final Val p = (Val)decl; final V v = node.value(); if (Objects.equals(v, p.value())) { if (node.childCount() == pattern.childCount()) { for (int i = 0; i < node.childCount(); ++i) { final Tree cn = node.childAt(i); final Tree, ?> cp = pattern.childAt(i); if (!matches(cn, cp, vars)) { return false; } } return true; } else { return false; } } else { return false; } } } /** * Expands {@code this} pattern with the given variable mapping. * * @param vars the variables to use for expanding {@code this} pattern * @return the expanded tree pattern * @throws NullPointerException if one of the arguments is {@code null} * @throws IllegalArgumentException if not all needed variables are part * of the {@code variables} map */ public TreeNode expand(final Map, Tree> vars) { return expand(_pattern, vars); } // Expanding the template. private static TreeNode expand( final Tree, ?> template, final Map, Tree> vars ) { final Map> paths = template.stream() .filter((Tree, ?> n) -> n.value() instanceof Var) .collect(toMap(Tree::childPath, t -> (Var)t.value())); final TreeNode tree = TreeNode.ofTree( template, n -> n instanceof Val val ? val.value() : null ); paths.forEach((path, decl) -> { final Tree replacement = vars.get(decl); if (replacement != null) { tree.replaceAtPath(path, TreeNode.ofTree(replacement)); } else { tree.removeAtPath(path); } }); return tree; } @Override public int hashCode() { return _pattern.hashCode(); } @Override public boolean equals(final Object obj) { return obj == this || obj instanceof TreePattern other && _pattern.equals(other._pattern); } @Override public String toString() { return _pattern.toParenthesesString(); } /* ************************************************************************* * Static factory methods. * ************************************************************************/ /** * Compiles the given tree pattern string. * * @param pattern the tree pattern string * @return the compiled pattern * @throws NullPointerException if the given pattern is {@code null} * @throws IllegalArgumentException if the given parentheses tree string * doesn't represent a valid pattern tree or one of the variable * names is not a valid (Java) identifier */ public static TreePattern compile(final String pattern) { return compile(pattern, Function.identity()); } /** * Compiles the given tree pattern string. * * @param pattern the tree pattern string * @param mapper the mapper which converts the serialized string value to * the desired type * @param the value type of the tree than can be matched by the pattern * @return the compiled pattern * @throws NullPointerException if the given pattern is {@code null} * @throws IllegalArgumentException if the given parentheses tree string * doesn't represent a valid pattern tree or one of the variable * names is not a valid (Java) identifier */ public static TreePattern compile( final String pattern, final Function mapper ) { return new TreePattern<>( TreeNode.parse(pattern, v -> Decl.of(v.trim(), mapper)) ); } /* ************************************************************************* * Java object serialization * ************************************************************************/ @Serial private Object writeReplace() { return new SerialProxy(SerialProxy.TREE_PATTERN, this); } @Serial private void readObject(final ObjectInputStream stream) throws InvalidObjectException { throw new InvalidObjectException("Serialization proxy required."); } void write(final ObjectOutput out) throws IOException { out.writeObject(_pattern); } @SuppressWarnings({"unchecked", "rawtypes"}) static Object read(final ObjectInput in) throws IOException, ClassNotFoundException { final var pattern = (TreeNode)in.readObject(); return new TreePattern(pattern); } /* ************************************************************************* * Pattern node classes. * ************************************************************************/ private static final char VAR_PREFIX = '$'; private static final char ESC_CHAR = '\\'; private static final Escaper ESCAPER = new Escaper(ESC_CHAR, VAR_PREFIX); /** * A sealed interface, which constitutes the nodes of a pattern tree. * The only two implementations of this class are the {@link Var} and the * {@link Val} class. The {@link Var} class represents a placeholder for an * arbitrary subtree and the {@link Val} class stands for an arbitrary * concrete subtree. * * @see Var * @see Val * * @param the node type the tree-pattern is working on */ public sealed interface Decl { /** * Returns a new {@link Decl} object with the mapped type {@code B}. * * @param mapper the mapping function * @param the mapped type * @return the mapped declaration * @throws NullPointerException if the mapping function is {@code null} */ Decl map(final Function mapper); static Decl of( final String value, final Function mapper ) { return Var.isVar(value) ? new Var<>(value.substring(1)) : new Val<>(mapper.apply(ESCAPER.unescape(value))); } } /** * Represents a placeholder (variable) for an arbitrary subtree. A * pattern variable is identified by its name. The pattern DSL * denotes variable names with a leading '$' character, e.g. {@code $x}, * {@code $y} or {@code $my_var}. * * @see Val * * @implNote * This class is comparable by its name. * @param the node type the tree-pattern is working on */ public record Var(String name) implements Decl, Comparable>, Serializable { @Serial private static final long serialVersionUID = 2L; /** * @param name the name of the variable * @throws NullPointerException if the given {@code name} is {@code null} * @throws IllegalArgumentException if the given {@code name} is not a * valid Java identifier */ public Var { if (!isIdentifier(name)) { throw new IllegalArgumentException(format( "Variable is not valid identifier: '%s'", name )); } } @Override @SuppressWarnings("unchecked") public Var map(final Function mapper) { return (Var)this; } @Override public int compareTo(final Var var) { return name.compareTo(var.name); } @Override public String toString() { return format("%s%s", VAR_PREFIX, name); } static boolean isVar(final String name) { return !name.isEmpty() && name.charAt(0) == VAR_PREFIX; } } /** * This class represents a constant pattern value, which can be part of a * whole subtree. * * @see Var * * @param the node value type * @param value the underlying pattern value */ public record Val(V value) implements Decl, Serializable { @Serial private static final long serialVersionUID = 2L; @Override public Val map(final Function mapper) { return new Val<>(mapper.apply(value)); } @Override public String toString() { return Objects.toString(value); } } }