系列入口:编程实战:类C语法的编译型脚本解释器(系列)-CSDN博客
前面已经介绍了Token、变量、变量表、表达式和语句,现在进入深水区,介绍编译过程,相当于解释型脚本的解释过程。
编译的好处是提前发现语法错误,然后可以重复执行几千万次,解释型对此无能为力。
目录
一、代码概览
二、成员变量
三、编译的主逻辑
一、代码概览
前面介绍的那些代码都是类,可以分开也可以放在一起,我的代表整个脚本的类是这样的:
这两张截图就是脚本类CScript的全貌,前面介绍的变量表、Token集合、表达式和语句都是这个类的子类(这是无关紧要的,整个代码放在一个名字空间中,也不会在里面掺入别的模块,所以子类还是独立类没有关系)。
public部分很简单,只有编译和执行的入口。private部分则是一系列互相递归的函数。
二、成员变量
代码:
public:string m_source;Variable::types return_type;string script_name;//如果是子函数,这是函数名long count_global_variable;//如果是子函数,这是全局变量个数(只能使用之前定义的全局变量)private:bool m_compiled;bool m_isFunction;//是否是子函数long m_execcount;//执行次数string m_msg;T_VARIABLE_BLOCK m_EnvVariables;//环境变量表,对于子函数,是参数vector<Sentence > m_sentences;//语句组vector<CScript > m_functions;//子函数vector<Expression*> m_NewExpressions;//所有new出来的表达式,析构时要清理
多解释一下:
类型 | 名称 | 用途 |
string | m_source | 脚本源码 |
Variable::types | return_type | 脚本或子函数的返回值 |
string | script_name | 如果是子函数,这是函数名,否则无意义 |
long | count_global_variable | 如果是子函数,这是可用的全局变量的个数,不过由于编译时按顺序编译,运行时又没有动态删除变量的机制,所以其实并不需要这个 |
bool | m_compiled | 脚本是否编译成功 |
bool | m_isFunction | 表明本对象是否是子函数 |
long | m_execcount | 运行计数,此值可以用做业务分析,常年不用的插件可以删掉 |
string | m_msg | 如果编译或运行出错,这里是出错信息 |
T_VARIABLE_BLOCK | m_EnvVariables | 对于脚本,是外部变量(环境变量),对于子函数,是调用参数 |
vector<Sentence > | m_sentences | 脚本或子函数的所有语句,概念上,脚本或函数就是一组语句 |
vector<CScript > | m_functions | 脚本的所有子函数 |
vector<Expression*> | m_NewExpressions | 内部机制,用于清理 |
从这个表我们对整个语言的语法结构就比较清晰了:
- 脚本就是入口参数+语句+返回值
- 语句就是控制结构和表达式
- 表达式就是加减乘除和函数调用,以及常量和变量
- 函数调用就是函数名、参数和返回值
- 参数就是表达式——又回到表达式
- 函数就是脚本——又回到最初
几条简单规则的无限递归——直到常量和变量。
三、编译的主逻辑
编译入口:
bool Compile(char const* _source, vector<pair<string, Variable > >* pEnvs = NULL){Init(_source);string str;CTokens tokens;T_VARIABLE_BLOCK currentGlobals;T_VARIABLE_BLOCK currentParams;T_VARIABLE_S curentVars;curentVars.T_VARIABLE_S_init(&m_EnvVariables, ¤tGlobals, -1, ¤tParams);//当前环境变量和全局变量在子函数中同样有效,必须独立存储try{if (!AddEnvVariableDelcare(pEnvs)){m_msg += "添加环境变量失败\r\n";return false;}if (!tokens.ToTokens(m_source)){m_msg += "编译失败\r\n";return false;}size_t pos = 0;if (!Build(tokens, curentVars, pos)){m_msg += "编译失败\r\n";return false;}}catch (string const& e){m_msg += e;Report(&tokens, curentVars, str);m_msg += str;return false;}catch (char const* s){m_msg += s;Report(&tokens, curentVars, str);m_msg += str;return false;}return true;}
主要分为三步:
- 设置外部变量(环境变量)
- 解析Token
- 执行编译Build()
从源码到Token这一步是独立完成的,后续所有操作都基于Token。
Build()的代码:
bool Build(CTokens& tokens, T_VARIABLE_S& vars, size_t& pos){bool isFunction = (0 != pos);while (pos < tokens.m_tokens.size()){if (isFunction){if (tokens.IsDelimiter(pos, "}")){break;}}m_sentences.resize(m_sentences.size() + 1);Token* pToken;if (!GetSentence(tokens, vars, m_sentences[m_sentences.size() - 1], pToken, pos))return false;if (!m_sentences[m_sentences.size() - 1].CheckSentence(m_source, vars, false))return false;}m_compiled = true;return true;}
逻辑非常简单:
- 如果是子函数而下一个Token是“}”,子函数结束
- 获取一个语句并检查
就这么简单。再往后才是真正的深水区。
(这里是结束,但不是整个系列的结束)