package gw.rating.flow.domain
uses com.google.common.collect.ImmutableList
uses com.google.common.collect.ImmutableSet
uses gw.api.locale.DisplayKey
uses gw.api.util.TypeUtil
uses gw.lang.reflect.IType
uses gw.rating.flow.util.RateflowTypeUtils
/**
* For IF and LOOP statements.
*
* Subclasses are required to implement the getter Statements which should return the list of
* statements in the body of the BlockStatement.
*
* Extracted getVariables() and friends from {@link IfStatement} so that we can use if for {@link LoopStatement} as
* well.
*/
abstract class BlockStatement extends StatementContainer {
protected static final var NUMBER_TYPE: Type = java.lang.Number.Type
protected construct(statements : ImmutableList) {
super(statements)
}
private static function canAssign(t1 : IType, t2 : IType) : boolean {
if (t1 == null or t2 == null) return false // prevent NPE if expression type unknown
if (gw.api.util.TypeUtil.isNominallyOrStructurallyAssignable(t1, java.math.BigDecimal)) {
return gw.api.util.TypeUtil.isNominallyOrStructurallyAssignable(NUMBER_TYPE, t2)
}
return TypeUtil.isNominallyOrStructurallyAssignable(t1, t2)
}
/**
* Appends variable declarations for all the local variables that are declared within the body of this
* BlockStatement to the generation context.
*
* @param context the generation context
*/
function addLocalVariableDeclarations(context : GenerationContext) {
var varsMap = getBlockLocalVariables(context)
if (Parent == null) {
// create a list of "var varName : varType"
varsMap.entrySet().stream()
.sorted(\o1 : Map.Entry, o2 : Map.Entry ->
o1.Key.compareTo(o2.Key)).forEach(\entry: Map.Entry -> {
var varName = entry.Key
var varType = entry.Value.Type
if (context.AddLocalVarDecls) {
context.append("var ${varName}: ${varType.Name}\n")
}
context.VarMap.put(varName, varType) // Modify the VarMap so type for variable is the (would be) decalred type.
})
}
}
/**
* Collects all the local variables that are declared in the specified block of statements that have not been previously
* declared. Excludes InScope, loop and loop index variables.
*
* @param context the generation context
*
* @return local variables used in the block that need to be declared, as a map (essentially) of name to type
*/
private function getBlockLocalVariables(context : GenerationContext) : Map {
var existingVars = ImmutableSet.copyOf(context.VarMap.Keys)
var varsMap = getVariables(context)
varsMap.retainWhereKeys(\k -> not existingVars.contains(k))
// Remove any InScope amd Loop related variables or any variable field/property reference.
varsMap.retainWhereKeys(\ k -> not (ParseUtil.isInScopeOrLoopVariableReference(k)))
// ... and persist the rest in the generation context
context.VarMap.putAll(varsMap.mapValues(\v -> v.Type))
return varsMap
}
/**
* Gets all the local variables used in this which have not previously been declared (i.e., they are not already in
* the context's variable map).
*
* Moved here from {@link IfStatement} so it can also be used by {@link LoopStatement}.
*
* @param context generation context
*
* @return local variables used in the block that need to be declared, as a map (essentially) of name to type
*/
private function getVariables(context: GenerationContext) : Map {
var varmap : Map = {}
// this is essentially a local "function" that has varmap in scope
var checkTypes = \ currentStatement : AssignmentStatement -> {
var currentVarKey = currentStatement.Variable.VariableReference
var currentType = currentStatement.getExpressionType(context)
var accumulatedVarAssignmentInfo = varmap.get(currentVarKey)
currentType = RateflowTypeUtils.getBoxedType(currentType)
if (accumulatedVarAssignmentInfo == null) {
var lhs = new VariableAssignmentInfo(currentType, currentStatement)
varmap.put(currentVarKey, lhs)
if (currentType == null and currentStatement.Operands.first() typeis LocalVariableOperand) {
// currentType == null can happen when the assignment is of the form localVar2 <- localVar1
// and localVar1 was defined inside the IF(s) that we're currently working on.
// We should have a locally-available type for the var on the RHS...but it might have to be
// widened. So we use whatever type we have, and register the dependency so if that
// type is widened later we can re-evaluate the dependent variable and widen it if necessary.
var localVar = currentStatement.Operands.first() as LocalVariableOperand
var key = localVar.VariableKey
var rhs = varmap.get(key)
if (rhs == null) {
// let's hope that context knows the type of the local variable from a previous declaration
if (lhs.Type == null) {
lhs.Type = context.VarMap.get(key)
}
} else {
rhs.registerDependency(lhs)
if (lhs.Type == null) {
lhs.Type = rhs.Type
}
}
}
} else {
if (currentType == null and currentStatement.Operands.first() typeis LocalVariableOperand) {
var key = (currentStatement.Operands.first() as LocalVariableOperand).VariableKey
currentType = varmap.get(key).Type ?: context.VarMap.get(key)
}
if (canAssign(accumulatedVarAssignmentInfo.Type, currentType)) {
accumulatedVarAssignmentInfo.registerAssignmentAttempt(currentStatement)
} else {
accumulatedVarAssignmentInfo.registerAssignmentAttempt(currentStatement)
if (canAssign(currentType, accumulatedVarAssignmentInfo.Type)) {
accumulatedVarAssignmentInfo.Type = currentType
} else {
var incompatibleElements = accumulatedVarAssignmentInfo.AssignmentAttempts
incompatibleElements.each(\element ->
element.addToContextOrThrow(context.Context, DisplayKey.get("Validation.Rating.RateRoutineDefinition.IncompatibleVariableTypes",
"${accumulatedVarAssignmentInfo.Type.RelativeName} (${accumulatedVarAssignmentInfo.Type})", "${currentType.RelativeName} (${currentType})",
"${currentVarKey}")))
}
}
}
}
// this is essentially a local "function" that has varmap in scope
var doStatements : block(s : List) // forward reference so doStatements can be recursive!
doStatements = \ _statements -> {
for (statement in _statements) {
if (statement typeis AssignmentStatement) {
checkTypes(statement)
} else if (statement typeis StatementContainer) {
doStatements(statement.Statements)
} else if (not (statement typeis CostDataRoundingStatement or statement typeis CommentStatement)) {
throw new IllegalStateException("getVariables() does not know how to handle ${typeof statement}")
}
}
}
doStatements(Statements)
return varmap
}
private static class VariableAssignmentInfo {
private var _type: IType as Type
private var _statements : Set = {}
private var _dependencies : Set = {}
property set Type(t : IType) {
_type = t
if (t != null) {
_dependencies.where(\ d -> canAssign(t, d.Type) and not canAssign(d.Type, t)).each(\ d -> {d.Type = t})
}
}
property get AssignmentAttempts() : List {
return _statements.map(\ s -> s.HighlightElement)
}
construct(t : IType, s : AssignmentStatement) {
Type = t
_statements.add(s)
}
function registerAssignmentAttempt(s : AssignmentStatement) {
_statements.add(s)
}
function registerDependency(d : VariableAssignmentInfo) {
_dependencies.add(d)
}
}
}