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 ... { } }