NamedFunction.java

/*
** Module   : NamedFunction.java
** Abstract : encapsulates the data and logic of named expressions
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------------------------Description------------------------------------
** 001 GES 20050601   @21358 Created initial version. Implements a scope,
**                           stores the expression text and the list
**                           of parameter names. Most importantly, it
**                           provides the functionality of executing the
**                           expression (first handling all variable
**                           assignments on behalf of the "caller" which
**                           is an expression in a rule-set).
** 002 GES 20050602   @21390 Converted from NamedExpression to
**                           NamedFunction.  Added an optional return
**                           variable, local variables and a list of
**                           expressions which are executed in order.
** 003 ECF 20050604   @21418 Create expressions using AstSymbolResolver's
**                           createExpression method.
** 004 GES 20050719   @21736 Added support for containing and executing
**                           rules, which provides the ability to embed
**                           complex logic inside a function.  This
**                           allows one to create "callable" rules.
** 005 ECF 20050728   @21971 Fixed scoping and added variable resets upon
**                           each execution. Enclosing scope now reports
**                           the scope of the caller which invoked this
**                           function. Local variables are reset to their
**                           initial values upon each execution of the
**                           function.
** 006 ECF 20080730   @39268 Ensure variables are cleared at the end of
**                           the execute() method. Reduced memory
**                           footprint.
** 007 GES 20130206          Reduced code duplication and reworked variable
**                           registration for the new initializer approach.
** 008 ECF 20150219          Protect integrity of enclosingScopes deque by popping in a finally
**                           block. Implemented generics.
** 009 HC  20160907          Implemented a simple tracing support for rules processing.
** 010 ECF 20171228          Improve performance by reducing context-local lookups.
** 011 CA  20221010          Avoid using iterators on ArrayList - instead, use a 'for' loop and get the 
**                           element by index, as iterators are more expensive in terms of memory and 
**                           performance.  Refs #6813
** 012 CA  20230223          Avoid the LinkedList overhead, use ArrayDeque instead, for 'enclosingScopes'. 
** 013 CA  20230531          Moved the scope state from the resolver to the actual scope; this avoids using
**                           a WeakHashMap to determine the scope's state associated with a certain resolver,
**                           as long as a single resolver instance uses that scope (common for conversion).
** 014 CA  20231129          Small changes to avoid 'invokeinterface' in the compiled bytecode.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.pattern;

import java.util.*;
import com.goldencode.expr.*;

/**
 * Provides a container to manage, store and execute a named function.  The
 * function can have one or more expressions which are executed. Such
 * functions can have an optional fixed list of named parameters,
 * local variables and a return variable which can be referenced in the 
 * given expressions and rules.  These parameters are replaced at execution
 * time which allows a "function call" type semantic for rule set users.
 * <p>
 * By virtue of the fact that rules can be added to the execute list,
 * functions support the ability to embed complex logic including control
 * flow (e.g. conditional execution and looping) and actions. Almost all of
 * the capabilities of rules can be used including nesting. The sole
 * exception is that rule types are completely ignored (even if specified
 * explicitly using an attribute).  This makes sense since there should be
 * no limitation on when this function can be called.  Such support allows
 * one to create "callable" rules using functions.
 * <p>
 * Please note that variable and return value initializers (the 
 * <code>init</code> attribute) are executed at the time that the function
 * is loaded rather than at runtime.  This means that parameters that are
 * passed to the function CANNOT be accessed in such initializers since the
 * parameters will not yet have values until the function is actually called.
 * <p>
 * <b>Known limitation:</b> currently, recursive calls to a single named
 * function are not supported, since only one local variable pool per named
 * function is maintained in the symbol resolver. This limitation includes
 * direct recursion (named function calls itself) as well as indirect
 * recursion (named function calls some other function which eventually calls
 * original named function). This limitation may be removed in future
 * development.
 *
 * @author    GES
 * @version   1.0.4
 */
class NamedFunction
extends Scope
{
   /** List of parameter names. */
   private ArrayList<String> params = new ArrayList<>(1);
   
   /** Stack of enclosing scopes;  scopes associated with call stack */
   private ArrayDeque<Scope> enclosingScopes = new ArrayDeque<>();
   
   /** The list of expressions to execute. */
   private ArrayList expr = new ArrayList(2);
   
   /**
    * The name of the return variable or <code>null</code> if the result 
    * of the last expression evaluation should be returned.
    */
   private String returnVar = null;
   
   /**
    * Constructor which assigns this container's enclosing scope and the
    * infix text of the first expression for this function.
    *
    * @param   parent
    *          Rule container which provides enclosing scope to this user
    *          function. May be <code>null</code>.
    * @param   expr
    *          The infix text of the expression associated with this
    *          instance.
    */
   NamedFunction(RuleContainer parent, String expr)
   {
      enclosingScopes.push(parent);
      registerExpression(expr);      
   }
   
   /**
    * Return the scope of the object in whose scope this function currently
    * is enclosed. This is typically the object which executed this function.
    *
    * @return  This function's enclosing scope. Should not be
    *          <code>null</code>.
    */
   public Scope getEnclosingScope()
   {
      // Although this technically can throw EmptyStackException, it is
      // not reported, since this would represent an architectural defect
      // rather than a legitimate runtime exception.
      return enclosingScopes.peek();
   }
   
   /**
    * Register a new expression for this named function.  This will be added
    * to the list of expressions/rules to be executed later in order.
    *
    * @param   text
    *          The infix text of the expression.
    */
   public void registerExpression(String text)
   {
      if (text != null)
      {
         expr.add(AstSymbolResolver.createExpression(text));
      }
   }
   
   /**
    * Register a new parameter for this named expression. If the parameter's
    * name is already in use within this rule container's scope, it cannot
    * be re-used and this method will fail.
    *
    * @param   name
    *          Name by which this parameter will be referenced in the
    *          expression.
    * @param   type
    *          Data type of the variable. Cannot be <code>null</code>.
    *
    * @throws  SymbolException
    *          if the parameter's name is already in use in this container's
    *          scope.
    * @throws  IllegalArgumentException
    *          if <code>type</code> is not provided.
    */
   public void registerParameter(String name, Class<?> type)
   {
      AstSymbolResolver resolver = AstSymbolResolver.getResolver();
      resolver.registerVariable(this, name, type, null, false);
      
      // store the parameter name in the correct order (this is essentially
      // our "signature")
      params.add(name);
   }
   
   /**
    * Register a variable which will be returned from this function. If the
    * return variable's name is already in use within this function's scope,
    * it cannot be re-used and this method will fail. If both
    * <code>type</code> and <code>expression</code> are provided,
    * <code>type</code> must match the return type of
    * <code>expression</code>.
    *
    * @param   name
    *          Name by which this variable will be referenced in expressions.
    * @param   type
    *          Data type of the variable. May be <code>null</code> if and
    *          only if <code>expression</code> is not <code>null</code>
    *          <b>and</b> <code>expression</code>'s return type is not
    *          <code>null</code>.
    * @param   expression
    *          Expression which will be used to initialize the variable each
    *          time it is {@link com.goldencode.expr.Variable#reset reset}
    *          by the pattern engine. May be <code>null</code> if and only
    *          if <code>type</code> is not <code>null</code>.
    *
    * @throws  SymbolException
    *          if the variable's name is already in use in this container's
    *          scope.
    * @throws  ExpressionException
    *          if <code>type</code> is a class which is not assignable from
    *          <code>expression</code>'s return type.
    * @throws  IllegalArgumentException
    *          if <code>type</code> is not provided and it cannot be
    *          determined from <code>expression</code>.
    *
    * @see     com.goldencode.expr.SymbolResolver#resetVariables
    */
   public void registerReturn(String name, Class<?> type, String expression)
   {
      registerVariable(name, type, expression);
      
      // save the name so we can lookup this variable later
      returnVar = name;
   }
   
   /**
    * Register a new user-defined local variable with this function. If the 
    * variable's name is already in use within this rule function's scope,
    * it cannot be re-used and this method will fail. If both
    * <code>type</code> and <code>expression</code> are provided, 
    * <code>type</code> must match the return type of
    * <code>expression</code>.
    *
    * @param   name
    *          Name by which this variable will be referenced in expressions.
    * @param   type
    *          Data type of the variable. May be <code>null</code> if and
    *          only if <code>expression</code> is not <code>null</code>
    *          <b>and</b> <code>expression</code>'s return type is not
    *          <code>null</code>.
    * @param   expression
    *          Expression which will be used to initialize the variable each
    *          time it is {@link com.goldencode.expr.Variable#reset reset}
    *          by the pattern engine. May be <code>null</code> if and only
    *          if <code>type</code> is not <code>null</code>.
    *
    * @throws  SymbolException
    *          if the variable's name is already in use in this function's
    *          scope.
    * @throws  ExpressionException
    *          if <code>type</code> is a class which is not assignable from
    *          <code>expression</code>'s return type.
    * @throws  IllegalArgumentException
    *          if <code>type</code> is not provided and it cannot be
    *          determined from <code>expression</code>.
    *
    * @see     com.goldencode.expr.SymbolResolver#resetVariables
    */
   public void registerVariable(String name, Class<?> type, String expression)
   {
      ExpressionInitializer ei = null;
      
      if (expression != null)
      {
         Expression expr = AstSymbolResolver.createExpression(expression);
         ei = new ExpressionInitializer(expr);
      }
      
      AstSymbolResolver resolver = AstSymbolResolver.getResolver();
      resolver.registerVariable(this, name, type, ei, false);
   }
   
   /**
    * Register a rule in the list of expressions/rules that the function
    * will execute in order.
    *
    * @param   expression
    *          Logical expression which will be compiled into the condition
    *          expression of the rule.
    * @param   type
    *          Type of rule to add: init, walk, post, etc., using the
    *          <code>RULE_*</code> constants.
    * @param   file
    *          The file the rule is defined in, used for tracing.
    * @param   line
    *          The file line the rule is defined at, used for tracing.
    *
    * @throws  IllegalArgumentException
    *          if the rule type is unrecognized.
    */
   public Rule registerRule(String expression, int type, String file, int line)
   {
      switch (type)
      {
         case RuleContainer.RULE_WALK:
         case RuleContainer.RULE_INIT:
         case RuleContainer.RULE_POST:
         case RuleContainer.RULE_DESCENT:
         case RuleContainer.RULE_NEXT_CHILD:
         case RuleContainer.RULE_ASCENT:
            break;
         default:
            throw new IllegalArgumentException("Invalid rule type: " + type);
      }
      
      Rule rule = new Rule(expression, type, file, line);

      expr.add(rule);
      
      return rule;
   }
   
   /**
    * Execute this named function, handling all user provided parameter
    * assignments before the actual execution.  The execution proceeds
    * through all expressions (defined via {@link #registerExpression}) and
    * rules (defined via {@link #registerRule}) in the order in which they
    * were added.  Rules are not simple expressions but instead they can
    * encode complex logical control flow (conditional processing and
    * looping) in addition to an arbitrary action execution.  This means
    * that there is no single return value that can be captured from a
    * rule.  For this reason, if a rule needs to be used to set a return
    * value for the function, then an explicit assignment will be needed.
    * If no explicit assignment to the return variable has occurred, then
    * the result of the last executed expression will be used as the return 
    * value.
    *
    * @param   resolver
    *          AST symbol resolver.
    * @param   callerScope
    *          Scope of the caller; becomes this function's current,
    *          enclosing scope for the duration of this execution.
    * @param   args
    *          The array of <code>Object</code> instances of the correct
    *          data type and in the correct order to match our parameter
    *          list.  Each element of the array will be assigned into the
    *          matching parameter in our parameter list before the expressions
    *          are executed.
    *
    * @return  The result of executing the expressions.
    *
    * @throws  ExpressionException
    *          if the expression cannot be compiled.
    * @throws  IllegalArgumentException
    *          if the number of passed parameters is different than the
    *          number of expected parameters.
    * @throws  IllegalStateException
    *          if this named function appears anywhere in the call stack
    *          leading up to this invocation.
    */
   public Object execute(AstSymbolResolver resolver, Scope callerScope, Object[] args)
   {
      // assert arguments passed match the number expected.
      if (args.length != params.size())
      {
         throw new IllegalArgumentException("Expected " + params.size() +
                                            " parameters, found " +
                                            args.length + ".");
      }
      
      // detect recursion and throw an exception if this named function
      // appears higher up in the call stack.
      Scope tmpScope = callerScope;
      while (tmpScope != null)
      {
         if (tmpScope == this)
         {
            throw new IllegalStateException(
               "Function recursion detected;  not currently supported");
         }
         tmpScope = tmpScope.getEnclosingScope();
      }
      
      enclosingScopes.push(callerScope);
            
      try
      {
         // reset local variables to their initial states (note:  this will
         // also reset parameters and return, since these are implemented
         // as variables registered to this function's scope).
         resolver.resetVariables(this, false);
         
         String[] list = (String[]) params.toArray(new String[0]);
         
         // assign each argument into the associated parameter
         for (int i = 0; i < list.length; i++)
         {
            Variable parm = resolver.getVariable(this, list[i]);
            parm.setValue(args[i]);
         }
         
         Object result = null;
         
         // execute all expressions in order
         int size = expr.size();
         Object item;
         for (int i = 0; i < size; i++)
         {
            item = expr.get(i);
            
            if (item instanceof Expression) 
            {
               Expression e = (Expression) item; 
               result = e.execute();
            }
            else
            {
               // must be a rule
               Rule r = (Rule) item;
               r.apply(resolver, RuleContainer.RULE_WALK);
            }
         }
         
         // honor the user-defined return variable if defined
         if (returnVar != null)
         {
            Variable ret = resolver.getVariable(this, returnVar);
            result = ret.getValue();
         }
         
         return result;
      }
      finally
      {
         // there is a slight chance that resetting variables can throw an exception, so protect
         // the integrity of enclosing scope deque in a finally block
         try
         {
            resolver.resetVariables(this, true);
         }
         finally
         {
            enclosingScopes.pop();
         }
      }
   }
   
   /**
    * Termination hook to allow resources to be cleaned up when this object is
    * about to go out of service.
    */
   public void cleanup()
   {
      params = null;
      enclosingScopes = null;
      expr = null;
   }
}