Компилятор языка формул

Модуль занимается полным циклом работ с формулами JetCalc (лексический анализ, семантический анализ и трансляция формул в исполняемый код).

Основные задачи модуля

  1. Анализ синтаксиса формул (parser.jison)

  2. Упрощение формул, в зависимости от контекста (column.jison, compile.jison)

  3. Вычисление формул (calculator.jison)

Второй тип модуля используется исключительно для оптимизации скорости работы и объемов вычисления.

Модуль написан с использованиемJISON(наиболее известный пример использования JISON - CoffeScript).

В качестве дополнительных инструментов для отладки и расширения функциональности модуля может использоватьсяJISON debugger

JISON состоит из следующих частей:

  1. Лексический анализатор (токенайзер)

  2. Парсер выражений.

  3. Реализация вычислений

Лексический анализатор (токенайзер)

%lex
%parse-param CONTEXT
%%

\s+                                            return '';
\+\s\+                                         return '+';
"*"                                            return '*';
"/"                                            return '/';
"-"                                            return '-';
"+"                                            return '+';
"^"                                            return '^';
"("                                            return '(';
")"                                            return ')';
"}"                                            return '}';
","                                            return ',';
"{"                                            return '{';
"AND"|"and"                                    return 'AND';
"OR"|"or"                                      return 'OR';
"NOT"|"not"|"!"                                return 'NOT';
"!="                                           return 'NE'; 
"<="                                           return 'LTE';
">="                                           return 'GTE';
">"                                            return 'GT';
"<"                                            return 'LT';
"=="                                           return 'EQ';
"true"|"TRUE"                                  return 'TRUE';
"false"|"FALSE"                                return 'FALSE'
"NULL"|"null"                                  return 'NULL';


periodin
|colin
|objin
|grpin
|divin
|coltagin
|objtagin
|rowtagin
|groupin                                       return 'BOOLFUNC'; 

treetagin
|pathin
|rowin
|rowgroupin
|rowsumgrpin                                   return 'U_BOOLFUNC';

[0-9]*[1-9]+[0-9]*("."[0-9]+)?\b                     
|[0]+"."[0-9]*[1-9]+[0-9]*?\b                  return 'NUMBER';

[0]+("."[0]*)?\b                               return 'ZNUMBER';

\".*?\" 
|\'.*?\'                                       return 'LITERAL';

'f.If'
|'if'                                          return 'IF';

[$@.].*?\?                                     return 'VARIABLE';

':'                                            return  'CASEDELIM';
';'                                            return  'EOC';

'forcol'
|'forobj'
|'fordiv'                                      return 'SWITCH_FUNC';

[A--Я0-9]+(?=^|$|[^\p{L}])                   return 'MIXED';

<<EOF>>                                        return 'EOF';

/lex

%left '+' '-'
%left '*' '/'
%left '^'
%left UMINUS
%left OR
%left AND
%left NOT

%%

Стоит обратить внимание на то, что в коде присутствуют токены начинающиеся с "U" (Uncalculated -невычисляемый), при этом они имеют зеркальные токены без такой приставки. Например, BOOLFUNC, U_BOOLFUNC. Позднее в парсере выражений - невычисляемые токены остаются такими, какими были, а вычисляемые вычисляются. Все парсеры между собой отличаются только перечислением функций которые нужно вычсислять и какие нужно оставить для более позднего вычисления.

Например в коде парсера колонок (column.jison) это выглядит - так:

periodin
|colin
|objin
|grpin
|divin
|coltagin
|objtagin
|groupin                    return 'BOOLFUNC'; 

treetagin
|pathin
|rowin
|rowtagin
|rowgroupin
|rowsumgrpin               return 'U_BOOLFUNC';

Все булевые функции, которые зависят от рядов - остаются, другие - вычисляются сразу. Крайние случаи parser.jison и calculator.jison: если в первом все функции и переменные невычисляемые, то во втором невычисляемых - нет.

Парсер выражений

Program: RESULT EOF { return $1; };

/* RETURN RESULT */

RESULT:
  MATH                             -> $1;
  | U_MATH                     -> $1;
;

/* NUMBERS */
NUMERIC:
  ZERO                            -> 0;
  | '('NUMBER')'                  -> $2;    
  | NUMBER                        -> Number($1);
  | CONSTANTA                     -> Number(CONTEXT[$1]);
  | FUNC '(' ARGS ')'             -> Number(LIB[$FUNC]($ARGS));
;

ZERO:
  ZNUMBER                      -> $1;
  | ZERO '-' '-' MATH          -> 0+Number($4);
  | MATH '-' '-' ZERO          -> Number($1)+0;  
  | U_MATH '*' ZERO            -> $3;
  | ZERO '*' U_MATH            -> $1;
  | U_MATH '/' ZERO            -> $3;
  | ZERO '/' U_MATH            -> $1;
  | MATH '*' ZERO              -> $3;
  | ZERO '*' MATH              -> $3;
  | MATH '/' ZERO              -> $3;
  | ZERO '/' MATH              -> $1;
  | ZERO '/' ZERO              -> $1;
  | ZERO '*' ZERO              -> $1;
  | ZERO '+' ZERO              -> $1;
  | ZERO '-' ZERO              -> $1;
  | '('ZERO')'                 -> $2;  
;

MATH:
  NUMERIC                         -> $1;
  | IF_THEN                       -> $1;
  | IF_THEN_ELSE                  -> $1;
  | SWITCH                        -> $1;  
  | MATH '+' MATH                 -> Number($1)+Number($3); 
  | MATH '-' MATH                 -> Number($1)-Number($3);
  | MATH '*' MATH                 -> Number($1)*Number($3);
  | MATH '/' MATH                 -> Number($1)/Number($3);
  | MATH '+' ZERO                 -> Number($1);
  | ZERO '+' MATH                 -> Number($3);
  | MATH '-' ZERO                 -> Number($1);
  | ZERO '-' MATH                 -> 0-Number($3);
  | '-' MATH %prec UMINUS         -> -Number($2);
  | '('MATH')'                    -> $2;  
;
/* Весь блок можно посмотреть в исходниках системы */

В данном блоке стоит обратить внимание на то, что модуль имеет отдельную структуру для работы с "0". С одной стороны это особенность, что деление на ноль здесь будет не бесконечность, а ноль. С другой стороны, в коде прописаны ситуации когда реализация простых правил ("что угодно" умножить на ноль = получается ноль и т.д.) позволяет значительно сократить объемы вычислений (не вычислять это "что угодно")

Как происходит вычисление: расмотрим простой пример 2+2*2

Поскольку мы не описали правил для сложений или умножения 2-х типов Number, то их тип повышается постепенно до Numeric и Math (на уровне Math арифметические операции мы уже описали) Почему произошло сначала умножение, а потом сложение - потому, что мы это описали в правилах приоритетов вычислений:

%left '+' '-'
%left '*' '/'
%left '^'
%left UMINUS
%left OR
%left AND
%left NOT

Последняя часть модуля - реализация вычислений

%{
  var IfThenElse =  function (ifExpr, thenExpr, elseExpr) {
     if (ifExpr) return thenExpr; else return elseExpr;
  };

  var Switch = function(funcName,Cases){
    var Indexed = {};
    var MatchFuncs = {
        forcol:'colin',
        forobj:'objin',
        fordiv:'divin',
        forperiod:'periodin'
    }
    Cases.forEach(function(Case){
        Indexed[Case[0]] = Case[1];
    })
    var Result = 0;
    for (var Args in Indexed){
        var Value = Indexed[Args];
        if (LIB[MatchFuncs[funcName]](Args.split(','))){
            Result = Value;
            break;
        }
    }
    return Result;
  }

var CONTEXT = {};

var LIB = {
    get:function(field){
      var Check = CONTEXT[field];
      if (Check) {
        if (Object.prototype.toString.call(Check)=="[object Array]"){
            return LIB._args(Check);
        } else {
            return Check+'';
        }
      }
      return 0;
    },
    _args:function(args){
      var result = [];
      args && args.forEach(function(a){ 
        result.push((a+'').replace(/["']/g,''));
      })
      return result;
    },
    _argsNumeric:function(args){
      var result = LIB._args(args);
      result && result.forEach(function(a,i){ 
        result[i] = Number(a);
      })
      return result;
    },
    _simpleCheck:function(key,args){
        args = LIB._args(args);
        var isOpposite = false;
        if (args[0]=="!"){
            args = args.splice(1);
            isOpposite = true;
        }
        var test = LIB.get(key);
        if (!isOpposite){
            return args.indexOf(test) !=-1;
        } else {
            return args.indexOf(test) ==-1;
        }        
    },
    _arrCheck:function(key,args){
        args = LIB._args(args);
        var grps = LIB.get(key);
        var intersect = grps.filter(function(n) {
            return args.indexOf(n) != -1;
        });
        return intersect.length>0;
    },
    _tagsCheck:function(key,args){
        args = LIB._args(args)||[];
        var tags = LIB.get(key)||[];
        var Result = false;
        tags.forEach(function(T){
            args.forEach(function(S){
                if (('/'+T+'/').match(new RegExp(S.replace('*','.*?')))){
                    Result = true;
                }
            })
        })
        return Result;
    },

  try{
    module.exports.setContext = function (myContext) {
        CONTEXT = myContext; 
    };
  } catch(e){
    //console.log("Нет контекста, пропишите его руками");
  }
%}

В этом модуле прописано, как ведет себя if, if...else, switch и прочии конструкции, но в целом - ничего интересного нет.

Несколько слов по копиляции парсеров. После того, как были внесены изменения в код jison парсеров, необходимо запустить консольную админку (node admin.js) и выбрать пункт компиляция парсеров

Окончание процесса компиляции сопровождается запуском mocha-тестов по проверке работы парсера Сами тесты находятся в JSON файле ./classes/calculator/jison/mocha/Documents/blank.js и имеют следующий формат:

{
  Formula: "if ( ( ismonth and not periodin( 11 ) ) or ( periodin (42,43,44,46,444,446,416,442) ) ,{1},{2})",
  Contexts:[
     {Context:{ismonth:true,period:19},Result : '1'},
     {Context:{ismonth:false,period:442},Result : '1'},
     {Context:{ismonth:true,period:11},Result : '2'}
  ]
}

Все тесты - должны проходиться

Last updated