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;
}
}