package com.guidewire.rating.flow.domain.util
uses gw.api.system.PCLoggerCategory
uses gw.rating.flow.domain.CalcRoutine
uses gw.rating.flow.domain.CompoundStatement
uses gw.rating.flow.domain.GenerationContext
uses gw.rating.flow.domain.IfStatement
uses gw.rating.flow.domain.LoopStatement
uses gw.rating.flow.domain.Statement
uses gw.util.Pair
uses gw.validation.PCValidationContext
/**
* Code generator for rate routines that breaks up large rate routines into a chain of methods
* The size of the methods is controlled by a configurable parameter.
*
* Usage is either new CalcRoutineMethodBaseCodeGenerator(routine).{@link #getExpression()} or
* new CalcRoutineMethodBaseCodeGenerator(routine).{@link #write(String, boolean)}.
*/
internal final class CalcRoutineMethodChainCodeGenerator extends CalcRoutineCodeGenerator {
static final var _logger = PCLoggerCategory.RATEFLOW
static final var LOOP_DATA_CLASS_NAME: Type = com.guidewire.rating.flow.domain.routines.LoopData.GenericType
final var _loops = new ArrayList()
var _methodCount = 0
construct(definition: CalcRoutineDefinition, orderedSteps: List,
validationContext: PCValidationContext, worksheets: boolean, useUniquePkgNameSuffix: boolean) {
super(definition, orderedSteps, validationContext, worksheets, useUniquePkgNameSuffix)
}
override protected function generate(): String {
var statements: List
try {
statements = Statements // Has the side effect, if there are errors, of throwing or altering validation context
} catch (e) {
// Validation context must be null.
throw new IllegalStateException(e.Message, e) // Because our tests want only IllegalStateException-s
}
if (ValidationContext?.Result?.hasErrors()) {
return null
}
var context = new GenerationContext(ValidationContext, GosuShop.createSymbolTable(true), GosuShop.createTypeUsesMap({}),
1, PolicyLinePatternCode, WorksheetsEnabled)
context.AddLocalVarDecls = false
context.CodegenStrategy = CHAIN
_methodCount = addExecuteMethod(context)
try {
generate(context, statements, 0, _methodCount)
} catch (e) {
_logger.error("Exception encountered in generating code for ${Code} v${Version}: ${e.Message}")
throw new IllegalStateException(e.Message, e) // Because our tests look for IllegalStateException only
}
if (ValidationContext?.Result?.hasErrors()) {
return null
}
var prologLineCount = prolog(context) // Do after generating: need context.VarMap local variables. Prepends.
epilog(context)
context.replace(CalcRoutineCodeGeneratorUtils.prettyPrint(context.GeneratedCode))
var headerLineCount = addHeaderComments(context) // Prepends
var prependLineCount = prologLineCount + headerLineCount
var sourceCode = context.GeneratedCode
fixStatementLineAndColumnNumbers(statements, sourceCode, prependLineCount)
return sourceCode
}
/**
* Main loop responsible for breaking up large rate routines into methods.
* The cut-points are generated on the fly. We keep track of the size of statements generated previously. If the
* current statement would cause us to exceed the method size threshold, we generate a call to the "next" method,
* end the current method, and start generating the next method.
*
* When we enter this method we have always just finished closing a method, and we start on the next method here.
*
* @param context generation context
* @param statements all statements that need generating
* @param currentStatementIndex index of current statement in statements
* @param methodIndex used as suffix for currently generated method. For statements that fit without splitting,
* it will be the same as _methodCount. It is needed to generate continuation methods for the
* complex statements that are being split and chained themselves and consequently rising _methodCount
* after the continuation method is being called.
*/
private function generate(context: GenerationContext, statements: List, currentStatementIndex: int,
methodIndex : int) {
/**
* Assumption: no simple statement ever exceeds MAX_METHOD_CODESIZE. If it does it means that MAX_METHOD_CODESIZE is
* too low and must be increased.
*/
if (statements.get(currentStatementIndex).CodeSize <= MAX_METHOD_CODESIZE) {
currentStatementIndex = generateFittingStatements(context, statements, currentStatementIndex, methodIndex)
if (currentStatementIndex < statements.size()) { // Have more statements
//simple method chaining
generate(context, statements, currentStatementIndex, _methodCount)
} // else all statements have been processed already
} else {
/**
* Should never be here for a simple statement. If we do land here for a simple statement, it means that the
* statement's size exceeded MAX_METHOD_SIZE, which violates our previous assumption. This will be caught in
* #generateAndSplitStatment() and an appropriate error message generated.
*/
var chainNextMethod = currentStatementIndex < statements.size() - 1
var chainedMethodNum = generateAndSplitStatement(context, statements.get(currentStatementIndex), methodIndex,
chainNextMethod)
if (chainNextMethod) {
//statement was split, next method was already called
generate(context, statements, currentStatementIndex + 1, chainedMethodNum)
}
}
}
/**
* Creates new method and puts in to is as many statements as possible.
*
* @param context generation context
* @param statements all statements that need generating
* @param currentStatementIndex index of the first statement that is not yet generated in the statements list
* @param methodIndex used as suffix for currently generated method
*
* @return index of the last statement that was already processed
*/
private function generateFittingStatements(context : GenerationContext, statements : List,
currentStatementIndex : int, methodIndex: int):int {
context.append("\n// Statements fit")
var numStatements = statements.size()
var currentCodeSize = 0
generateMethodStart(context, methodIndex)
while (currentStatementIndex < numStatements and
(currentCodeSize + statements.get(currentStatementIndex).CodeSize) <= MAX_METHOD_CODESIZE) {
var statement = statements.get(currentStatementIndex)
currentCodeSize += statement.CodeSize
statement.generate(context)
context.append("\n")
currentStatementIndex++
}
var chainNextMethod = currentStatementIndex < numStatements
generateMethodEnd(context, chainNextMethod)
return currentStatementIndex
}
/**
* Creates new method and handles the case when a single statement is too large to fit it
*
* @param context generation context
* @param statement that is to be generated (and split)
* @param methodIndex index of currently generated method
*
* @return index of the method used for chaining. If there was no chaining, that's current method count
*/
private function generateAndSplitStatement(context: GenerationContext, statement: Statement, methodIndex: int,
chainNextMethod: boolean): int {
var start = context.beginCodeGen()
context.append("\n// Splitting large statement")
switch (typeof statement) {
case IfStatement:
methodIndex = generateIf(context, statement, methodIndex, chainNextMethod)
break
case LoopStatement:
methodIndex = generateLoop(context, statement, methodIndex, chainNextMethod)
break
case CompoundStatement:
// Compound is body of loop or if statement, never simple, so if there is chaining needed, it's already been handled
methodIndex = generateCompound(context, statement, methodIndex)
break
default:
throw new IllegalStateException("Attempt to split a simple statement: MaxRateRoutineCodeSize value of ${MAX_METHOD_CODESIZE} is too low.")
}
var end = context.endCodeGen()
statement.setStartAndEnd(start, end)
return methodIndex
}
/**
* Creates a loop-statement or if-statement body
*
* @param context generation context
* @param compound statement to be generated
* @param methodIndex index of currently generated method
*/
private function generateCompound(context: GenerationContext, compound: CompoundStatement, methodIndex: int): int {
context.append("\n// Generating compound")
generate(context, compound.Statements, 0, methodIndex)
return _methodCount
}
/**
* If the IF statement exceeds the method size limit, we make each branch a method.
* We must chain the current method to continue after the IfStatement.
* It is required by caller that on exit from this method we are inside of a method - either the method we were in at
* entry or, f we have closed that method then we must start a new method and chain the old one to the new.
*
* @param context generation context
* @param ifStatement the IF statement
* @param isLast if true then this is the last statement in list of statements
*
* @return the code size of the last method in progress
*/
private function generateIf(context: GenerationContext, ifStatement : IfStatement, methodIndex: int,
chainNextMethod: boolean): int {
generateMethodStart(context, methodIndex)
ifStatement.addLocalVariableDeclarations(context) // For grabbing all the block local variables
// Make each branch a method
var elseSeen = false
var branches = new ArrayList>()
for (cond in ifStatement.Conditions) {
if (elseSeen) {
throw new IllegalStateException("Cannot have if, elseif or else after else")
}
_methodCount++
var branchMethodId = _methodCount
branches.add(new Pair(branchMethodId, cond.Second))
}
var sequences = new List[branches.size()]
for (branch in branches index i) {
sequences[i] = Collections.singletonList(branch.First)
}
new IfStatementMethodCodeGenerator(ifStatement, sequences).generate(context)
generateMethodEnd(context, chainNextMethod)
var chainedMethodNum = _methodCount
for (branch in branches) {
generate(context, Collections.singletonList(branch.Second), 0, branch.First)
}
return chainedMethodNum
}
/**
* Both at code generation time and at routine run time, we need to keep track of the loops a method is inside of. At
* the start of each method we generate code to retrieve all the loop variables of these loops.
*
* @param context generation context
* @param loopStatement statement to be generated
* @param methodIndex if set to true, call to next method is added at the end
* @param chainNextMethod id of the chained method (if chainNextMethod is set to true)
* @return id of the chained method, if chaining, current methodCount otherwise
*/
private function generateLoop(context : GenerationContext, loopStatement : LoopStatement, methodIndex: int,
chainNextMethod: boolean) : int {
context.append("\n// Loop statement")
generateMethodStart(context, methodIndex)
loopStatement.addLocalVariableDeclarations(context) // For grabbing all the block local variables
_methodCount++
var loopBodyMethodNumber = _methodCount
// Don't log to worksheet as this is just an artifact to enable method chaining
context.append("\nvar _loopData = new ${LOOP_DATA_CLASS_NAME}<${loopStatement.LoopVariable.ReferenceTypeName}>")
.append("(${loopStatement.IterableOperand.VariableReference})")
context.append("\n_loops.add(_loopData)")
_loops.add(loopStatement) // Add loop to generation time list
if (context.WorksheetsEnabled) {
context.append('\n')
context.WorksheetHelper.wrapStatement(loopStatement, \ -> {
context.append("\nfor (${loopStatement.LoopVariable.VariableReference} in ${loopStatement.IterableOperand.VariableReference} ")
.append("index ${loopStatement.LoopIndexVariableReference}) {")
// Create the iteration record and the iteration itself.
context.append('\n')
context.WorksheetHelper.wrapStatement(loopStatement.LoopBody, \-> {})
context.append("\n_loopData.Index = ${loopStatement.LoopIndexVariableReference}")
context.append("\n// Loop statement: call loop body method")
context.append("\n_method${loopBodyMethodNumber}()")
context.append("\n}\n") // Close for () {
})
} else {
context.append("\nfor (${loopStatement.LoopVariable.VariableReference} in ${loopStatement.IterableOperand.VariableReference} ")
.append("index ${loopStatement.LoopIndexVariableReference}) {")
context.append("\n_loopData.Index = ${loopStatement.LoopIndexVariableReference}")
context.append("\n// Loop statement: call loop body method")
context.append("\n_method${loopBodyMethodNumber}()")
context.append("\n}\n") // Close for()
}
context.append("\n_loops.remove(_loops.size() - 1)") // Remove from run time
/**
* The loop body method:
*
* _method${methodCount}() {
* var _loopData: LoopData
*
* _loopData = _loops.get(0)
* var ${loopIndexVariable[0]} = _loopData.Index
* var ${loopVariable[0]} = _loopData.Iterable.get(_loopData.Index) as ${loopVariable[0].Type.Name}
*
* _loopData = _loops.get(1)
* var ${loopIndexVariable[1]} = _loopData.Index
* var ${loopVariable[1]} = _loopData.Iterable.get(loopData.Index) as ${loopVariable[1].Type.Name}
*
* ...
* // Loop body
* ...
* }
*/
context.VarMap.put(loopStatement.LoopVariable.VariableKey, loopStatement.LoopVariable.ReferenceType)
context.VarMap.put(loopStatement.LoopIndexVariableKey, LoopStatement.INT_ITYPE)
generateMethodEnd(context, chainNextMethod)
var continuationMethodNo = _methodCount
context.append("\n// Loop statement: loop body method")
generate(context, loopStatement.LoopBody.Statements, 0, loopBodyMethodNumber)
context.VarMap.remove(loopStatement.LoopIndexVariableKey)
context.VarMap.remove(loopStatement.LoopVariable.VariableKey)
_loops.remove(_loops.size() - 1) // Remove from generation time
return continuationMethodNo
}
private function generateMethodStart(context: GenerationContext, methodIndex: int) {
context.append("\nprivate function _method${methodIndex}() { \n")
generateLoopVariables(context)
}
private function generateMethodEnd(context: GenerationContext, chainNextMethod: boolean) {
if (chainNextMethod) {
_methodCount++
context.appendNewlineIfNeeded()
context.appendNewlineIfNeeded().append("_method${_methodCount}()")
}
context.append("\n}\n")
}
/**
* Generates access for all the loop variables of loops that this method is "contained" in.
*
* var __loopData: LoopData
*
* __loopData = _loops.get(0)
* var ${loopIndexVariable[0]} = __loopData.Index
* var ${loopVariable[0]} = __loopData.Iterable.get(__loopData.Index) as ${loopVariable[0].Type.Name}
*
* __loopData = _loops.get(1)
* var ${loopIndexVariable[1]} = __loopData.Index
* var ${loopVariable[1]} = __loopData.Iterable.get(__loopData.Index) as ${loopVariable[1].Type.Name}
*
* ...
*
* @param context the generation context
*
* @return the code size of the generated loop variable data
*/
private function generateLoopVariables(context: GenerationContext): int {
if (_loops.HasElements) {
context.append("\nvar __loopData: ${LOOP_DATA_CLASS_NAME}\n")
for (loop in _loops index idx) {
if (context.WorksheetsEnabled) {
context.append("__loopData = _loops.get(${idx})\n")
context.append("var ${loop.LoopIndexVariableReference} = _log.let(\"${loop.LoopIndexVariableReference}\", ")
.append("__loopData.Index)\n")
context.append("var ${loop.LoopVariable.VariableReference} = ")
.append("_log.let<${loop.LoopVariable.ReferenceType.Name}>(\"${loop.LoopVariable.VariableReference}\", ")
.append("\\ -> __loopData.Iterable.get(${loop.LoopIndexVariableReference}) ")
.append("as ${loop.LoopVariable.ReferenceType.Name})")
} else {
context.append("__loopData = _loops.get(${idx})\n")
context.append("var ${loop.LoopIndexVariableReference} = __loopData.Index\n")
context.append("var ${loop.LoopVariable.VariableReference} = __loopData.Iterable.get(${loop.LoopIndexVariableReference})")
.append(" as ${loop.LoopVariable.ReferenceType.Name}\n")
}
context.append('\n')
}
}
return _loops.size()
}
// ---------------------------------------------------------------------------------------//
// Methods for the various boilerplate elements in the generated code //
// ---------------------------------------------------------------------------------------//
private function prolog(context: GenerationContext): int {
var sb = new StringBuilder()
sb.append("package ${PackageName}\n\n")
addImports(sb)
sb.append("final class ${RoutineName} implements ").append(RATE_ROUTINE_INTERFACE).append(" {\n")
addIdentification(sb)
addRatingFunctionSources(sb)
addExecutionLoggerSymbols(sb)
addParameterDeclarations(sb)
addLocalVariableDeclarations(sb, context)
addProperties(sb)
var prolog = sb.toString()
var lineCount = prolog.countMatches("\n")
context.prepend(prolog)
return lineCount
}
private function addExecuteMethod(context: GenerationContext): int {
context.append("override function execute(")
.append("${CalcRoutine.RATE_BOOK}: entity.RateBook, ")
.append("${COST_DATA_WRAPPER}: ${COST_DATA_WRAPPER_TYPE_NAME}, ")
.append("${PARAMETER_SET_ARGUMENT_NAME}: java.util.Map) {\n")
addParameterInitializations(context)
/**
* Intentionally *not* logging to worksheet, so that worksheets remain same as for the single method case.
*/
_methodCount++
var methodCount = _methodCount
context.append("_method${methodCount}()")
context.append("\n}")
return methodCount
}
override function addLocalVariableDeclarations(sb: StringBuilder, context: GenerationContext) {
super.addLocalVariableDeclarations(sb, context)
sb.append("var _loops = new java.util.ArrayList<${LOOP_DATA_CLASS_NAME}>()\n\n") // Run time loop tracking
}
private function epilog(context: GenerationContext) {
context.append("\n}") // close the opening class ... {
}
}