digitorum.ru

Как меня найти

Профиль

icq: 4415944

flex & bison: пишем генератор кода

bison, c and cpp, flex

Постановка задачи.

Написать шаблонизатор и "компилятор", который переводит описанный шаблон в "php" и "javascript" код.

Язык "шаблонирования".

Операторы:

  • var - переменная
<var[foo]> преобразуется в $input['foo']
<var[foo->bar]> преобразуется в $input['foo']['bar']
<var[:foo]> преобразуется в $foo

 

  • const
<const[authed]>

 

  • print
<print <var[foo]>>

 

  • if (if-else)
<if (<var[foo]> gt 1 || <var[foo]> == "bar") && <var[foo]> == <var[bar]>>
  ...
<else>
  ...
</if>

 

  • switch
<switch <var[foo]>>
  <case "bar">
    ...
  </case>
  <case "baz">
    ...
  </case>
</switch>

 

  • each
<each <var[foo]> as <var[:k]> <var[:v]>>
  ...
</each>

 

  • include - в ключить в текст шаблона текст другого шаблона (name указывается относительно "-sp")
<include name="/tmp/tpl.txt" input="<var[foo]>">

 

  • file - указать в какой файл нужно записать результат работы (name указывается относительно "-dp")
<file name="/output_file_name.txt">

 

Логические выражения:

  • && - логическое И
  • || - логическое ИЛИ
  • gt - больше
  • ge - больше или равно
  • lt - меньше           
  • le - меньше или равно
  • == (eq) - равно
  • != - не равно

Методы:

  • verbal_rus("one", "two", "many")
  • xmlescape()
<print <var[foo].methodname().nextmethodname()>>

 

Выбор средств для реализации.

Регулярными выражениями тут явно не обойтись. Для разбора данных конструкций необходим лексер и грамматика.

В качестве первого был выбран flex, а для генерации кода и описания грамматики - bison. Можно было все это описать и на php (тем более наработки уже есть), но я все таки рискнул сделать иной выбор.

Почему не flexc++ и bisonc++?  Я, скажем так, не очень хорошо знаю c++ (но использовать его в проекте все равно буду, так как мне нужен хотя бы тип "string" ).

Описаине "command line options" и запуск приложения.

./template
  -l язык, код которого необходимо построить (php|js)
  -p префикс переменной (в описании var это $input)
  -s резульатат работы будет импользован как подшаблон или нет (1|0)
  -sp раздел с шаблонами (см. описание include)
  -dp раздел в который необходимо записать результат (см. описание file)
  < путь до файла

 

Например:

./template -p "\$foo" -l "js" -sp "./" -dp "./" < tests/template.txt

 

Пример компиляции.

Исходник:

<if <var[foo->bar]> != <var[:innerVar]>>
	Условие пройдено: <print <var[foo->bar]>>.
</if>

 

php:

<?php if($root['foo']['bar'] != $localVar) { ?> Условие пройдено:<?php print $root['foo']['bar']; ?><?php } ?>

 

js (создается callback):

var f = function($root) { return '' + ( $root['foo']['bar'] != $localVar ? (' Условие пройдено:' + $root['foo']['bar'] + '') : '' ) + ''; }

 

template.l: Описание правил для разбора текста на токены (исходный код aplha-версии).

%{

#include <iostream>
#include <string>
#include <vector>
#include "template.tab.h"

using namespace std;

string stringBuffer;
string attributeNameBuffer;
vector<int> previousStates;

//#define YY_DECL extern "C" int yylex();
extern "C" int yylex();
char* string_to_char(string str);

int pop_last_state() {
	int state = previousStates[previousStates.size() - 1];
	previousStates.pop_back();
	return state;
}

void push_last_state(int state) {
	previousStates.push_back(state);
}

%}

%s SCRIPT
%s TEXT
%s VAR
%s STRING_DQ
%s STRING_SQ
%s ATTRIBUTES
%s ATTRIBUTE_VALUE
%s METHODS
%s PARAMETERS

WS					[ \t\r]+
NUMBER					[0-9.,]+

%%

<INITIAL,TEXT>{
	{WS}*\<file{WS}*		{ BEGIN(ATTRIBUTES); return T_OUTPUT_FILE_NAME_OPEN; }	
	{WS}*\<include{WS}*		{ BEGIN(ATTRIBUTES); return T_INCLUDE_OPEN; }
	{WS}*\<if{WS}*			{ BEGIN(SCRIPT); return T_IF_OPEN; }
	{WS}*\<else>			{ return T_IF_ELSE; }
	{WS}*\<\/if>			{ return T_IF_CLOSE; }
	{WS}*\<print{WS}*		{ BEGIN(SCRIPT); return T_PRINT_OPEN; }
	{WS}*\<each{WS}*		{ BEGIN(SCRIPT); return T_EACH_OPEN; }
	{WS}*\<\/each>			{ return T_EACH_CLOSE; }
	{WS}*\<switch{WS}*		{ BEGIN(SCRIPT); return T_SWITCH_OPEN; }
	{WS}*\<\/switch>{WS}*		{ return T_SWITCH_CLOSE; }
	{WS}*\<case{WS}*		{ BEGIN(SCRIPT); return T_CASE_OPEN; }
	{WS}*\<\/case>{WS}*		{ return T_CASE_CLOSE; }
	\n				;
	{WS}+				{ yylval.sval = string_to_char(" "); return T_TEXT; }
	.				{ yylval.sval = strdup(yytext); return T_TEXT; }
	<<EOF>>				{ return T_END; }
}

<SCRIPT>{
	\<var\[				{ push_last_state(SCRIPT); BEGIN(VAR); return T_VAR_OPEN; }
	\<const\[			{ push_last_state(SCRIPT); BEGIN(VAR); return T_CONST_VAR_OPEN; }
	{NUMBER}			{ yylval.sval = strdup(yytext); return T_NUMBER; }
	\"				{ push_last_state(SCRIPT); BEGIN(STRING_DQ); stringBuffer = "\""; }
	'				{ push_last_state(SCRIPT); BEGIN(STRING_SQ); stringBuffer = "'";  }
	\(				{ return T_OPEN_RBRACKET; }
	\)				{ return T_CLOSE_RBRACKET; }
	&&				{ return T_AND; }
	\|\|				{ return T_OR; }
	(==|eq)				{ return T_EQ; }
	!=				{ return T_NOT_EQ; }
	gt				{ return T_GT; }
	ge				{ return T_GE; }
	lt				{ return T_LT; }
	le				{ return T_LE; }
	as				{ return T_AS; }
	>				{ BEGIN(TEXT); return T_TAG_CLOSE; }
	{WS}*				;
}

<VAR>{
	([:_a-zA-Z0-9]+(->)*)+		{ yylval.sval = strdup(yytext); return T_TEXT; }
	\]				;
	\.				{ BEGIN(METHODS); }
	>				{ BEGIN(pop_last_state()); return T_VAR_CLOSE; }
	.				;
}

<METHODS>{
	[_a-z]+				{ yylval.sval = strdup(yytext); return T_METHOD; }
	\.				;
	\(				{ BEGIN(PARAMETERS); }
	>				{ BEGIN(pop_last_state()); return T_VAR_CLOSE; }
}

<PARAMETERS>{
	\"				{ push_last_state(PARAMETERS); BEGIN(STRING_DQ); stringBuffer = "\""; }
	'				{ push_last_state(PARAMETERS); BEGIN(STRING_SQ); stringBuffer = "'";  }
	\)				{ BEGIN(METHODS); }
	,				;
}

<STRING_DQ>{
	\\\"				{ stringBuffer += yytext; }
	\"				{ BEGIN(pop_last_state()); yylval.sval = string_to_char(stringBuffer + yytext); return T_STRING; }
	.				{ stringBuffer += yytext; }
}

<STRING_SQ>{
	\\'				{ stringBuffer += yytext; }
	'				{ BEGIN(pop_last_state()); yylval.sval = string_to_char(stringBuffer + yytext); return T_STRING; }
	.				{ stringBuffer += yytext; }
}

<ATTRIBUTES>{
	[a-z]+				{ attributeNameBuffer += yytext; }
	=				{ BEGIN(ATTRIBUTE_VALUE); yylval.sval = string_to_char(attributeNameBuffer); attributeNameBuffer=""; return T_ATTR_NAME; }
	>				{ BEGIN(TEXT); return T_TAG_CLOSE; }
	{WS}*				;
}

<ATTRIBUTE_VALUE>{
	\"				{ push_last_state(ATTRIBUTE_VALUE); BEGIN(STRING_DQ); stringBuffer = "\""; }
	'				{ push_last_state(ATTRIBUTE_VALUE); BEGIN(STRING_SQ); stringBuffer = "'";  }
	\<var\[				{ push_last_state(ATTRIBUTE_VALUE); BEGIN(VAR); return T_VAR_OPEN; }
	\<const\[			{ push_last_state(SCRIPT); BEGIN(VAR); return T_CONST_VAR_OPEN; }
	>				{ BEGIN(TEXT); return T_TAG_CLOSE; }
	{WS}*				{ BEGIN(ATTRIBUTES); }
}

%%

 

template.y: Описание грамматики и правил генерации кода (исходный код aplha-версии).

%{

#include <iostream>
#include <cstring>
#include <stdio.h>
#include <string>
#include <vector>
#include <map>
#include <fstream>
// aptitude install libboost-all-dev
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>

// используем std::
using namespace std;

// корневой раздел для обработанных шаблонов
string targetPath = "";
// корневой раздел для исходников шаблонов
string sourcePath = "";
// файл для сохранения шаблона
string output = "";
// путь до шаблонизатора
string templater;
// имя "входной" переменной (см. описание шаблонизатора)
string varprefix = "$input";
// язык, код которого нужно сгенерировать
string language = "php";
// является ли шаблон подшаблоном
bool subtemplate = false;
// вхождения в E_MAIN
vector<vector<string> > statesMain;
// вхождения в E_CASES
vector<vector<string> > statesCase;
// список методов, которые были накастаны на "что-то"
vector<vector<string> > methodsList;
// список входных параметров метода
vector<string> methodParemetersList;
// список атрибутов оператора
map<string,string> attributes; 
// список методов
map<string, map<string,string> > definedMethods = {
	{
		"php",
		{
			{"xmlescape", "htmlspecialchars"},
			{"verbal_rus", "digits_verbal_rus"}
		}
	},
	{
		"js",
		{
			{"xmlescape", "Tools.FuncList.XmlEscape"},
			{"verbal_rus", "Tools.FuncList.VerbalRus"}
		}
	}
	
};
// список предопределенных констант
map<string, map<string,string> > definedConstants = {
	{
		"php",
		{
			{"page", "$_SERVER['PHP_SELF']"},
			{"authed", "user_is_authed()"}
		}
	},
	{
		"js",
		{
			{"page", "window.location.pathname"},
			{"authed", "Tools.User.Id"}
		}
	}
	
};

extern "C" int yylex();
extern "C" int yyparse();

// сообщение об ошибке
void yyerror(const char *s);


/*** работа со стороками ***/


// перевести строку в char*
char* string_to_char(string str) {
	char *cstr = new char[str.length() + 1];
	strcpy(cstr, str.c_str());
	return cstr;
}

// убираем все кавычки из строки
string string_from_attribute(string str) {
	str = str.substr(1, str.size() - 2);
	boost::replace_all(str, "\\", "");
	return str;
}


/*** работа с "векторами-сторок" и "векторами-векторов-строк" ***/


// добавить вектор в slist
void slist_push(vector<vector<string> > & slist) {
	vector<string> p;
	slist.push_back(p);
}

// добавить в последний вектор slist строку
void slist_append_to_last(vector<vector<string> > & slist, string str) {
	slist[slist.size() - 1].push_back(str);
}

// объединить вектор строк в одну строку через разделитель
string slist_implode(vector<string> p, string delimiter) {
	string text;
	for(int i=0; i<p.size(); i++) {
		if(i!=0) {
			text += delimiter;
		}
		text += p[i];
	}
	return text;
}

// вытащить вектор из slist и первратить его в строку
string slist_pop_last(vector<vector<string> > & slist) {
	if(!slist.size()) {
		return "";
	}
	vector<string> p = slist[slist.size() - 1];
	slist.pop_back();
	return slist_implode(p, "");
}


/*** выполнение системных команд ***/


// выполнить системную команду и получить результат ее работы
string execcmd(string cmd) {
	string result = "";
	char buffer[128];
	FILE* pipe = popen(string_to_char(cmd), "r");
	if (!pipe) {
		return "";
	}
	while(!feof(pipe)) {
		if(fgets(buffer, 128, pipe) != NULL)
			result += buffer;
	}
	pclose(pipe);
	return result;
}


/*** построение шаблона ***/


// собрать имя переменной в правильный вид
string build_var_name(string str) {
	if(str.substr(0, 1) == ":") {
		return "$" + str.substr(1, str.size() - 1);
	}
	boost::replace_all(str, "->", "']['");
	return varprefix + "['" + str + "']";
}

// собрать имя константы в правильный вид
string build_const_name(string str) {
	if(definedConstants[language].count(str)) {
		return definedConstants[language][str];
	}
	return "\"\"";
}

// собрать конструкцию "print"
string build_print(string str) {
	if(language == "php") {
		return "<?php print " + str + "; ?>";
	} else {
		return "' + " + str + " + '";
	}
}

// собрать конструкцию "if"
string build_if(string expr, string actions) {
	if(language == "php") {
		return "<?php if(" + expr + ") { ?>" + actions + "<?php } ?>";
	} else {
		return "' + ( " + expr + " ? ('" + actions + "') : '' ) + '";
	}
}

// собрать конструкцию "if-else"
string build_if_else(string expr, string actions, string elseActions) {
	if(language == "php") {
		return "<?php if(" + expr + ") { ?>" + actions + "<?php } else { ?>" + elseActions + " } ?>";
	} else {
		return "' + ( " + expr + " ? ('" + actions + "') : ('" + elseActions + "') ) + '";
	}
	
}

// собрать конструкцию "foreach"
string build_foreach(string var, string k, string v, string actions) {
	if(language == "php") {
		return "<?php foreach(" + var + " as " + k + " => " + v + ") { ?>" + actions + "<?php } ?>";
	} else {
		return "' + (function() { var html = ''; $.each(" + var + ", function(" + k + ", " + v + ") { html += '" + actions + "'}); return html; })() + '";
	}
}

// собрать конструкцию "case"
string build_case(string cond, string actions) {
	if(language == "php") {
		return " case (" + cond + "): ?>" + actions + "<?php break; ";
	} else {
		return " case (" + cond  + "): return ('" + actions + "'); break; "; 
	}
}

// собрать конструкцию "switch"
string build_switch(string var, string cases) {
	if(language == "php") {
		return "<?php switch(" + var + ") { " + cases + " } ?>";
	} else {
		return "' + (fucntion() { switch(" + var + ") { " + cases + " } }()) + '";
	}
}

// накастать методы на var
string build_apply_methods(string var) {
	for(int i=0; i<methodsList.size(); i++) {
		vector<string> p = methodsList[i];
		if(definedMethods[language].count(p[0])) {
			for(int j=0; j<p.size(); j++) {
				if(j == 0) {
					var = definedMethods[language][p[j]] + "(" + var;
				} else {
					var += "," + p[j];
				}
			}
			var += ")";
		}
	}
	methodsList.clear();
	return var;
}

// распарсить подшаблон
string build_subtemplate() {
	string cmd = templater;
	string var = varprefix;
	if(attributes.count("name") == 0) {
		return "";
	}
	if(attributes.count("input") != 0) {
		var = attributes["input"];
	}
	cmd += " -p \"\\" + var + "\"";
	cmd += " -l \"" + language + "\"";
	cmd += " -sp \"" + targetPath + "\"";
	cmd += " -tp \"" + sourcePath + "\"";
	cmd += " -s 1";
	cmd += " < " + sourcePath + "/" + string_from_attribute(attributes["name"]);
	return execcmd(cmd);
}

// финальные действия над шаблоном
string build_template(string text) {
	if(language == "js" && !subtemplate) {
		return "var f = function(" + varprefix + ") { return '" + text + "'; } ";
	}
	return text;
}

// установить файл для вывода
void template_set_output_file_name() {
	if(!subtemplate && attributes.count("name") != 0) {
		output = attributes["name"];
	}
}

// сохраняем шаблон
void template_store(string text) {
	string path = targetPath;
	if(output != "") {
		std::vector<std::string> pathParts;
		// заменяем все кавычки
		output = string_from_attribute(output);
		boost::split(pathParts, output, boost::is_any_of("/"));
		for(int i=0; i<pathParts.size(); i++) {
			if(pathParts[i] != "") {
				path += "/" + pathParts[i];
				if(i == (pathParts.size() - 1)) {
					std::ofstream file;
					file.open(path);
					file << text;
					file.close();
				} else {
					boost::filesystem::path dir(path.c_str());
					if(!boost::filesystem::create_directory(dir)) {
						// do error stuff
					}
				}
			}
		}
	} else {
		cout << text;
	}
}

%}

// переопределение yylval
%union {
	char *sval;
}

// опредления токенов
%token <sval> T_TEXT
%token <sval> T_NUMBER
%token <sval> T_STRING
%token <sval> T_METHOD
%token <sval> T_ATTR
%token <sval> T_ATTR_NAME
%token T_IF_OPEN T_IF_ELSE T_IF_CLOSE
%token T_EACH_OPEN T_AS T_EACH_CLOSE
%token T_SWITCH_OPEN T_SWITCH_CLOSE T_CASE_OPEN T_CASE_CLOSE
%token T_OUTPUT_FILE_NAME_OPEN;
%token T_INCLUDE_OPEN
%token T_PRINT_OPEN
%token T_VAR_OPEN T_CONST_VAR_OPEN T_VAR_CLOSE
%token T_TAG_CLOSE
%token T_END
%token T_EQ T_NOT_EQ T_GT T_GE T_LT T_LE
%token T_AND T_OR
%token T_OPEN_RBRACKET T_CLOSE_RBRACKET

// задаем левую ассоциативность для операций (...) http://www.opennet.ru/docs/RUS/bison_yacc/bison_yacc-prog.html.gz
%left T_EQ T_NOT_EQ T_GT T_GE T_LT T_LE T_AND T_OR

// определения типов нетерминальных символов
%type <sval> E_EXPR
%type <sval> E_VAR

%%

E_MAIN:
	/* empty */ {
		slist_push(statesMain);
	}
	| E_MAIN E_SCRIPT
	| E_MAIN T_END {
		template_store(build_template(slist_pop_last(statesMain)));
		return 0; // all done
	}


E_SCRIPT:
	E_IF
	| E_IF_ELSE
	| E_INCLUDE
	| E_PRINT
	| E_EACH
	| E_SWITCH
	| E_OUTPUT_FILE
	| T_TEXT {
		slist_append_to_last(statesMain, string($1)); 
	}


E_OUTPUT_FILE:
	T_OUTPUT_FILE_NAME_OPEN E_ATTRIBUTES T_TAG_CLOSE {
		template_set_output_file_name();
	}


E_SWITCH:
	T_SWITCH_OPEN E_VAR T_TAG_CLOSE E_CASES T_SWITCH_CLOSE {
		slist_append_to_last(statesMain, build_switch(string($2), slist_pop_last(statesCase)));
	}


E_CASES:
	/* empty */ {
		slist_push(statesCase);
	}
	| E_CASES T_CASE_OPEN E_VAR T_TAG_CLOSE E_MAIN T_CASE_CLOSE {
		slist_append_to_last(statesCase, build_case(string($3), slist_pop_last(statesMain)));
	}


E_EACH:
	T_EACH_OPEN E_VAR T_AS E_VAR E_VAR T_TAG_CLOSE E_MAIN T_EACH_CLOSE {
		slist_append_to_last(statesMain, build_foreach(string($2), string($4), string($5), slist_pop_last(statesMain)));
	}


E_PRINT:
	T_PRINT_OPEN E_VAR T_TAG_CLOSE {
		slist_append_to_last(statesMain, build_print(string($2)));
	}


E_INCLUDE:
	T_INCLUDE_OPEN E_ATTRIBUTES T_TAG_CLOSE {
		slist_append_to_last(statesMain, build_subtemplate());
	}


E_ATTRIBUTES:
	/* empty */
	| E_ATTRIBUTES T_ATTR_NAME E_VAR {
		attributes.insert(make_pair(string($2), string($3)));
	}


E_IF_ELSE :
	T_IF_OPEN E_EXPR T_TAG_CLOSE E_MAIN T_IF_ELSE E_MAIN T_IF_CLOSE {
		string stateMain2 = slist_pop_last(statesMain);
		string stateMain1 = slist_pop_last(statesMain);
		slist_append_to_last(statesMain, build_if_else(string($2), stateMain1, stateMain2));
	}


E_IF:
	T_IF_OPEN E_EXPR T_TAG_CLOSE E_MAIN T_IF_CLOSE {
		slist_append_to_last(statesMain, build_if(string($2), slist_pop_last(statesMain)));
	}


E_EXPR:
	E_VAR
	| T_OPEN_RBRACKET E_EXPR T_CLOSE_RBRACKET {
		$$ = string_to_char(" ( " + string($2) + " ) ");
	}
	| E_EXPR T_EQ E_EXPR {
		$$ = string_to_char(string($1) + " == " + string($3));
	}
	| E_EXPR T_GE E_EXPR {
		$$ = string_to_char(string($1) + " >= " + string($3));
	}
	| E_EXPR T_LE E_EXPR {
		$$ = string_to_char(string($1) + " <= " + string($3));
	}
	| E_EXPR T_GT E_EXPR {
		$$ = string_to_char(string($1) + " > " + string($3));
	}
	| E_EXPR T_LT E_EXPR {
		$$ = string_to_char(string($1) + " < " + string($3));
	}
	| E_EXPR T_AND E_EXPR {
		$$ = string_to_char(string($1) + " && " + string($3));
	}
	| E_EXPR T_OR E_EXPR {
		$$ = string_to_char(string($1) + " || " + string($3));
	}
	| E_EXPR T_NOT_EQ E_EXPR {
		$$ = string_to_char(string($1) + " != " + string($3));
	}


E_VAR:
	T_VAR_OPEN T_TEXT E_METHODS T_VAR_CLOSE {
		$$ = string_to_char(build_apply_methods(build_var_name(string($2))));
	}
	| T_CONST_VAR_OPEN T_TEXT E_METHODS T_VAR_CLOSE {
		$$ = string_to_char(build_apply_methods(build_const_name(string($2))));
	}
	| T_STRING
	| T_NUMBER


E_METHODS:
	/* empty */
	| E_METHODS T_METHOD E_PARAMETERS {
		vector<string> p;
		p.push_back(string($2));
		if(methodParemetersList.size() > 0) {
			p.push_back(slist_implode(methodParemetersList, ","));
		}
		methodsList.push_back(p);
	}

E_PARAMETERS:
	| E_PARAMETERS T_STRING {
		methodParemetersList.push_back(string($2));
	}
%%

main( int argc, char* argv[] )
{
	//yydebug=1;
	int i;
	string option = "";
	templater = string(argv[0]);
	for(i=1; i<argc; ++i) {
		if( i % 2 == 0) {
			if(option == "-p") {
				varprefix = argv[i];
			} else if(option == "-l") {
				language = argv[i];
			} else if(option == "-s") {
				subtemplate = (bool)argv[i];
			} else if(option == "-sp") {
				sourcePath = argv[i];
			} else if(option == "-dp") {
				targetPath = argv[i];
			}
		} else {
			option = argv[i];
		}
	}
	yyparse();
}

void yyerror(const char *str) {
	fprintf(stderr, "ошибка: %s\n", str);
}

 

Компиляция.

bison -d -v -t template.y
flex template.l
g++ -std=c++11 template.tab.c lex.yy.c -o template -lfl -lboost_filesystem  -lboost_system

 

Вот собственно и все . После тестирования опишу чуть более подробно о потенциальных проблемах и путях их решения.