import { ArtifactContext, ArtifactExprContext, AssignmentContext, BooleanExprContext, BuiltInOpContext, CaseContext, CasesExprContext, CompOpContext, DefineSetContext, DefinitionContext, FuzzyExprContext, FuzzySymContext, GlobalBindContext, IndicatorCategoryContext, IndicatorContext, IndicatorExprContext, InvertOpContext, InvokedOpContext, LocalBindContext, MacroExprContext, MaxOpContext, MinOpContext, NodeTypeContext, OpDefContext, OpTypedParamContext, SetConstContext, SetCrispContext, SetDownContext, SetTrapContext, SetTriContext, SetUpContext, StartContext } from "../antlr/CertusParser"; import { CertusContext, ContextError, FZ, Operation, TypedParameter } from "../model/Context"; import { CharStreams, CommonTokenStream } from "antlr4ts"; import { CertusLexer } from "../antlr/CertusLexer"; import { CertusParser } from "../antlr/CertusParser"; import { CertusVisitor } from "../antlr/CertusVisitor"; // generated interface import { ErrorNode } from "antlr4ts/tree/ErrorNode"; import { ParseTree } from "antlr4ts/tree/ParseTree"; import { RuleNode } from "antlr4ts/tree/RuleNode"; import { TerminalNode } from "antlr4ts/tree/TerminalNode"; import { Artifact, Indicator, IndicatorCategory, NodeType } from "../model/Argument"; import { CertusErrorListener } from "./CertusErrorListener"; export class EvaluationError extends Error { constructor(msg: string) { super(msg); } } export class CertusEvaluator implements CertusVisitor { private ctx: CertusContext = null; constructor() { // empty } public evaluate(input: string, initCtx: CertusContext): CertusContext { // Store for access within the class this.ctx = initCtx; // Parse the input string using Antlr const chars = CharStreams.fromString(input); const lexer = new CertusLexer(chars); const tokens = new CommonTokenStream(lexer); const parser = new CertusParser(tokens); // remove ANTLR’s default listener, which write to the console lexer.removeErrorListeners(); parser.removeErrorListeners(); // Attach our own error listener for custom error logging const err = new CertusErrorListener(); lexer.addErrorListener(err); parser.addErrorListener(err); // Kick off parsing process const tree = parser.start(); // Kick's off evaluation, Antlr will call methods of this // class using the visitor protocol to evaluate the expression try { this.visit(tree); } catch (e) { if (e instanceof EvaluationError || e instanceof RangeError || e instanceof ContextError) { this.ctx.addEvaluationErrors(e); } else { throw e; // Some other error, propagate it } } // Log any syntax errors collected during parse this.ctx.addSyntaxErrors(...err.errors); // After the visitor finishes, the class's ctx member has // been updated with the results of the computation return this.ctx; } /** * Expands macros in expressions like: "C1 is #FUSE". Returns the * expanded macro. * * Warning. This will produce very large strings that will take a long * time for Certus to parse for argument steps with more than 2 or 3 children. * * @param input Certus expression * @returns an updated Certus expression containing macro expansions */ public expandMacro(input: string): string { const M = this.ctx.getMacros(); input = input.trim(); for (const m of M) { const ps = `^([a-zA-Z][a-zA-Z0-9_]*)\\s+(is)\\s+\#(${m.name.toUpperCase()})$`; const pat = new RegExp(ps, "i"); const theMatch = input.match(pat); if (theMatch) { const [full, target, op, macroName] = theMatch; return m.expand(this.ctx, target); } } return input; } visit(tree: ParseTree): void { if (tree instanceof RuleNode) { return tree.accept(this); } else if (tree instanceof TerminalNode) { return this.visitTerminal(tree); } else if (tree instanceof ErrorNode) { return this.visitErrorNode(tree); } else { throw new EvaluationError("Unknown ParseTree node type"); } } visitStart = (ctx: StartContext) => { if (ctx.globalBind()) { this.visitGlobalBind(ctx.globalBind()); } else { if (ctx.localBind()) { this.visitLocalBind(ctx.localBind()); } this.visitAssignment(ctx.assignment()); } } visitLocalBind = (ctx: LocalBindContext) => { if (ctx.definition()) { for (const d of ctx.definition()) { this.visitDefinition(d); } } else { throw new EvaluationError(`No definitions provided in local binding block (WITH ... END).`) } } visitGlobalBind = (ctx: GlobalBindContext) => { if (ctx.definition()) { for (const d of ctx.definition()) { this.visitDefinition(d); } } else { throw new EvaluationError(`No definitions provided in global binding block (GLOBAL ... END).`) } } visitAssignment(ctx: AssignmentContext): void { // Check we are permitted to assign the value const target = ctx.ID().text; if (!this.ctx.hasRoot(target)) { throw new EvaluationError(`Assignment target ${target} is not the root of the current scope.`); } let result: FZ; if (ctx.fuzzyExpr()) { // Direct assignment e.g., C1 is min(low, vlow) result = this.visitFuzzyExpr(ctx.fuzzyExpr()); } else if (ctx.macroExpr()) { result = this.visitMacroExpr(ctx.macroExpr()); } else { throw new EvaluationError(`Unknown alternative for assignment expression`); } // throws if assignment would produce an invalid set this.checkValidSet(result); this.ctx.setRootValuation(result); } visitDefinition(ctx: DefinitionContext): void { if (ctx.ID()) { const expr = this.visitFuzzyExpr(ctx.fuzzyExpr()); expr.name = ctx.ID().text; this.ctx.addNamedSet(ctx.ID().text, expr); } else if (ctx.opDef()) { let expr: CasesExprContext | FuzzyExprContext; if (ctx.fuzzyExpr()) { expr = ctx.fuzzyExpr(); } else { throw new EvaluationError(`Unknown alternative for definition: neither cases nor fuzzy expression`) } const opName: string = ctx.opDef().ID().text; const params: TypedParameter[] = ctx.opDef().opTypedParam().map(o => this.visitOpTypedParam(o)); const myOp = new Operation(opName, params, expr); myOp.check(); this.ctx.addNamedOp(myOp); } else { throw new EvaluationError(`Unknown alternative for definition`); } } visitOpDef?: (ctx: OpDefContext) => void; visitOpTypedParam(ctx: OpTypedParamContext): TypedParameter { const name = ctx.ID().text; const type = this.visitNodeType(ctx.nodeType()); return new TypedParameter(name, type); } visitNodeType(ctx: NodeTypeContext): NodeType { return NodeType.fromString(ctx.text); } visitBooleanExpr(ctx: BooleanExprContext): boolean { if (ctx.getChild(0).text == '(' && ctx.getChild(2).text == ')') { return this.visitBooleanExpr(ctx.booleanExpr(0)); } else if (ctx.NOT()) { return !this.visitBooleanExpr(ctx.booleanExpr(0)); } else if (ctx.AND()) { return this.visitBooleanExpr(ctx.booleanExpr(0)) && this.visitBooleanExpr(ctx.booleanExpr(1)); } else if (ctx.OR()) { return this.visitBooleanExpr(ctx.booleanExpr(0)) || this.visitBooleanExpr(ctx.booleanExpr(1)); } else if (ctx.indicatorExpr()){ return this.visitIndicatorExpr(ctx.indicatorExpr()); } else if (ctx.artifactExpr()) { return this.visitArtifactExpr(ctx.artifactExpr()); } else if (ctx.LT()) { const left = this.visitFuzzyExpr(ctx.fuzzyExpr(0)); const right = this.visitFuzzyExpr(ctx.fuzzyExpr(1)); const score = this.ctx.rankSets(left, right); return score < 0; } else if (ctx.LEQ()) { const left = this.visitFuzzyExpr(ctx.fuzzyExpr(0)); const right = this.visitFuzzyExpr(ctx.fuzzyExpr(1)); const score = this.ctx.rankSets(left, right); return score <= 0; } else if (ctx.GT()) { const left = this.visitFuzzyExpr(ctx.fuzzyExpr(0)); const right = this.visitFuzzyExpr(ctx.fuzzyExpr(1)); const score = this.ctx.rankSets(left, right); return score > 0; } else if (ctx.GEQ()) { const left = this.visitFuzzyExpr(ctx.fuzzyExpr(0)); const right = this.visitFuzzyExpr(ctx.fuzzyExpr(1)); const score = this.ctx.rankSets(left, right); return score >= 0; } else if (ctx.NEQ()) { const left = this.visitFuzzyExpr(ctx.fuzzyExpr(0)); const right = this.visitFuzzyExpr(ctx.fuzzyExpr(1)); const score = this.ctx.rankSets(left, right); return score != 0; } else if (ctx.EQ()) { const left = this.visitFuzzyExpr(ctx.fuzzyExpr(0)); const right = this.visitFuzzyExpr(ctx.fuzzyExpr(1)); const score = this.ctx.rankSets(left, right); return score == 0; } else if (ctx.EXACTLY()) { const left = this.visitFuzzyExpr(ctx.fuzzyExpr(0)); const right = this.visitFuzzyExpr(ctx.fuzzyExpr(1)); return left.identical(right); } else if (ctx.ASSIGN()) { const left = this.visitFuzzyExpr(ctx.fuzzyExpr(0)); const right = this.visitFuzzyExpr(ctx.fuzzyExpr(1)); return right.contains(left); } else if (ctx.CONTAIN()) { const left = this.visitFuzzyExpr(ctx.fuzzyExpr(0)); const right = this.visitFuzzyExpr(ctx.fuzzyExpr(1)); return left.contains(right); } else if (ctx.OVERLAP()) { const left = this.visitFuzzyExpr(ctx.fuzzyExpr(0)); const right = this.visitFuzzyExpr(ctx.fuzzyExpr(1)); return left.overlaps(right); } else { throw new Error("Unknown alternative for BooleanExpr"); } } visitArtifactExpr = (ctx: ArtifactExprContext) => { return this.visitArtifact(ctx.artifact()) != null; } visitIndicatorExpr = (ctx: IndicatorExprContext) => { if (ctx.ASSIGN() && ctx.indicator().length == 1) { // $foobar is DANGER const left = this.visitIndicator(ctx.indicator()[0]); const cat = this.visitIndicatorCategory(ctx.indicatorCategory()); return this.ctx.indicatorIsCategory(left, cat); } else if (ctx.indicator().length == 1) { const left = this.visitIndicator(ctx.indicator()[0]); const val = parseFloat(ctx.FLOAT().text); if (ctx.EQ() || ctx.EXACTLY()) { return left.value == val } else if (ctx.LT()) { return left.value < val } else if (ctx.LEQ()) { return left.value <= val } else if (ctx.GT()) { return left.value > val } else if (ctx.GEQ()) { return left.value >= val } else if (ctx.NEQ()) { return left.value != val } else { throw new EvaluationError(`Unknown alterative for indicator comparison against number: ${ctx.text}`); } } else if (ctx.indicator().length == 2) { const left = this.visitIndicator(ctx.indicator()[0]); const right = this.visitIndicator(ctx.indicator()[1]); if (ctx.EQ() || ctx.EXACTLY()) { return left.value == right.value } else if (ctx.LT()) { return left.value < right.value } else if (ctx.LEQ()) { return left.value <= right.value } else if (ctx.GT()) { return left.value > right.value } else if (ctx.GEQ()) { return left.value >= right.value } else if (ctx.NEQ()) { return left.value != right.value } else { throw new EvaluationError(`Unknown alterative for indicator to indicator comparison: ${ctx.text}`); } } else { throw new EvaluationError(`Unknown alternative for indicator comparison`) } } visitCasesExpr(ctx: CasesExprContext): FZ { for (let i = 0; i < ctx.case().length; i++) { let [match, fuzz] = this.visitCase(ctx.case(i)); if (match) { return fuzz; // use first match, if found } } // Nothing matched, fall back on otherwise case return this.visitFuzzyExpr(ctx.fuzzyExpr()) } visitCase(ctx: CaseContext): [boolean, FZ] { const matched = this.visitBooleanExpr(ctx.booleanExpr()); if (matched) { return [matched, this.visitFuzzyExpr(ctx.fuzzyExpr())] } else { return [matched, null]; } } visitMacroExpr(ctx: MacroExprContext): FZ { const foundMacro = this.ctx.getMacro(ctx.ID().text) if (foundMacro) { return foundMacro.evaluate(this.ctx); } else { throw new EvaluationError(`Unknown macro: ${ctx.ID().text}`); } } visitFuzzyExpr(ctx: FuzzyExprContext): FZ { if (ctx.INTERSECT()) { // E.g., c1 intersect c2 const children = ctx.fuzzyExpr(); const leftOp = this.visitFuzzyExpr(children[0]); const rightOp = this.visitFuzzyExpr(children[1]); return leftOp.intersect(rightOp); } else if (ctx.UNION()) { // E.g., c1 union c2 const children = ctx.fuzzyExpr(); const leftOp = this.visitFuzzyExpr(children[0]); const rightOp = this.visitFuzzyExpr(children[1]); return leftOp.union(rightOp); } else if (ctx.casesExpr()) { // cases [bool] -> [fuzzy], [bool] -> [fuzzy], otherwise [fuzzy] return this.visitCasesExpr(ctx.casesExpr()); } else if (ctx.defineSet()) { // trap(...), up(...), down(...), etc. return this.visitDefineSet(ctx.defineSet()); } else if (ctx.builtInOp()) { // min(...), max(...), invert(...), comp(...), etc. return this.visitBuiltInOp(ctx.builtInOp()); } else if (ctx.invokedOp()) { // Any user defined operator return this.visitInvokedOp(ctx.invokedOp()); } else if (ctx.fuzzySym()) { // Any other fuzzy symbol from context (e.g., node ID, canonical set name) return this.visitFuzzySym(ctx.fuzzySym()); } else { throw new EvaluationError(`Unknown alternative for fuzzy expression: ${ctx.text}`); } } visitInvokedOp(ctx: InvokedOpContext): FZ { const opName = ctx.ID(0).text; const foundOp = this.ctx.getNamedOp(opName); if (!foundOp) { throw new EvaluationError(`Unknown operator: '${opName}'`) } const savedCtx = this.ctx.clone(); for (let i = 1; i < ctx.ID().length; i++) { const passedName = ctx.ID(i).text; const passedType = this.ctx.getChildType(passedName); // Look up correspond param in the operators definition const param = foundOp.params[i-1]; if (!NodeType.checkSubType(param.type, passedType)) { throw new EvaluationError(`Data types are incompatible for invocation of operator ${opName}, ${passedName}:${passedType.valueOf()} is not compatible with ${foundOp.name}:${param.type.valueOf()}`) } // TODO - Implement parameter type fuzzy set as well? // Now bind to the current context const fuzz = this.ctx.getChildValuation(passedName).clone(); this.ctx.addChild(param.name, param.type, fuzz); } // Now compute the expression using the new bindings let result: FZ; if (foundOp.expr instanceof CasesExprContext) { result = this.visitCasesExpr(foundOp.expr as CasesExprContext) } else if (foundOp.expr instanceof FuzzyExprContext) { result = this.visitFuzzyExpr(foundOp.expr as FuzzyExprContext) } else { throw new EvaluationError(`Unknown alternative encountered when evaluating operator ${opName}, expression is neither cases expression nor fuzzy expression`); } // Replace the context so that we don't continue with the bindings we have this.ctx = savedCtx; return result; } visitFuzzySym(ctx: FuzzySymContext): FZ { const target: string = ctx.ID().text; if (this.ctx.hasChild(target)) { return this.ctx.getChildValuation(target); } else if (this.ctx.hasNamedSet(target)) { return this.ctx.getNamedSet(target); } else { throw new EvaluationError(`Symbol '${target}' is not in the current scope`); } } visitBuiltInOp(ctx: BuiltInOpContext): FZ { if (ctx.minOp()) { return this.visitMinOp(ctx.minOp()); } else if (ctx.maxOp()) { return this.visitMaxOp(ctx.maxOp()); } else if (ctx.invertOp()) { return this.visitInvertOp(ctx.invertOp()); } else if (ctx.compOp()) { return this.visitCompOp(ctx.compOp()); } else { throw new EvaluationError(`Unknown built-in operator: ${ctx.text}`); } } visitMinOp(ctx: MinOpContext): FZ { const args: FZ[] = ctx.fuzzyExpr().map(f => this.visitFuzzyExpr(f)); args.forEach(f => this.checkValidSet(f)); // throws if any argument is not valid return args.reduce((smallest, curr) => curr.lt(smallest) ? curr : smallest, args[0]); } visitMaxOp(ctx: MaxOpContext): FZ { const args: FZ[] = ctx.fuzzyExpr().map(f => this.visitFuzzyExpr(f)); args.forEach(f => this.checkValidSet(f)); // throws if any argument is not valid return args.reduce((biggest, curr) => curr.gt(biggest) ? curr : biggest, args[0]); } visitInvertOp = (ctx: InvertOpContext): FZ => { return this.ctx.computeSetInversion(this.visitFuzzyExpr(ctx.fuzzyExpr())); } visitCompOp = (ctx: CompOpContext): FZ => { return this.visitFuzzyExpr(ctx.fuzzyExpr()).complement(); } visitDefineSet(ctx: DefineSetContext): FZ { if (ctx.setTrap()) { return this.visitSetTrap(ctx.setTrap()); } else if (ctx.setTri()) { return this.visitSetTri(ctx.setTri()); } else if (ctx.setUp()) { return this.visitSetUp(ctx.setUp()); } else if (ctx.setDown()) { return this.visitSetDown(ctx.setDown()); } else if (ctx.setConst()) { return this.visitSetConst(ctx.setConst()); } else if (ctx.setCrisp()) { return this.visitSetCrisp(ctx.setCrisp()); } else { throw new EvaluationError("Unknown alternative for defineSet"); } } visitSetTrap(ctx: SetTrapContext): FZ { const args = ctx.FLOAT().map(t => parseFloat(t.text)); return this.ctx.buildTrap(args[0], args[1], args[2], args[3]); } visitSetTri(ctx: SetTriContext): FZ { const args = ctx.FLOAT().map(t => parseFloat(t.text)); return this.ctx.buildTri(args[0], args[1], args[2]); } visitSetUp(ctx: SetUpContext): FZ { const args = ctx.FLOAT().map(t => parseFloat(t.text)); return this.ctx.buildUp(args[0], args[1]); } visitSetDown(ctx: SetDownContext): FZ { const args = ctx.FLOAT().map(t => parseFloat(t.text)); return this.ctx.buildDown(args[0], args[1]); } visitSetConst(ctx: SetConstContext): FZ { const arg = parseFloat(ctx.FLOAT().text); return this.ctx.buildConstant(arg); } visitSetCrisp(ctx: SetCrispContext): FZ { const args = ctx.FLOAT().map(t => parseFloat(t.text)); return this.ctx.buildTrap(args[0], args[0], args[1], args[1]); } visitIndicator(ctx: IndicatorContext): Indicator { const id = ctx.ID().map(t => t.text).join("_"); if (this.ctx.hasIndicator(id)) { return this.ctx.getIndicator(id); } else { throw new EvaluationError(`Indicator ${id} not found in context.`); } } visitArtifact(ctx: ArtifactContext): Artifact | null { const id = ctx.ID().map(t => t.text).join("_"); if (this.ctx.hasArtifact(id)) { return this.ctx.getArtifact(id); } else { return null; } } visitIndicatorCategory(ctx: IndicatorCategoryContext): IndicatorCategory { const id = ctx.ID().text; if (this.ctx.hasIndicatorCategory(id)) { return this.ctx.getIndicatorCategory(id); } else { throw new EvaluationError(`Indicator category ${id} not found in context.`); } } visitChildren(node: RuleNode): void { throw new Error("Method not implemented."); } visitTerminal(node: TerminalNode): void { throw new Error("Method not implemented."); } visitErrorNode(node: ErrorNode): void { throw new Error("Method not implemented."); } /** * Throws if the fuzzy set is invalid for purpose of Certus evaluation. * @param f Set to check. */ private checkValidSet(f: FZ): void { if (!f) { throw new EvaluationError(`Fuzzy set is not valid: ${f}`); } if (f.isEmpty()) { throw new EvaluationError(`Operation produces an empty set: ${f.name}`) } if (!f.isNormal()) { throw new EvaluationError(`Operation produces a sub-normal set: ${f.name}`); } if (!f.isConvex()) { throw new EvaluationError(`Operation produces a non-convex set: ${f.name}`); } } }