创建自己的脚本语言
开发背景
在某些情况下,在考虑业务背景及目标客户群体时,“提供一个可量身定做的开发环境”是一个值得探索的方向。在这样的环境中,“一系列复杂的语法规范”、“权限控制机制”以及“资源利用情况”都可以根据具体需求进行高度量身定制。在此背景下,“选择了一种较为直接且易于实施的方式”来构建一门自定义语言执行引擎。
设计必要语法元素
下面让我们了解即将开发的语言中的一些核心语法规则,并通过实践掌握这些基础概念;如果您对此感到枯燥或复杂可以直接跳转至'演示代码'部分进行学习。
表达式
为了简化说明起见, 表达式仅限于两种格式. 其一为直接数值, 包括100、“100”、true、false等; 其二是基于内置对象的属性或经过函数计算的结果. 另一种则是依赖运算符的复合表达式, 此时须明确运算顺序以避免歧义. 例如, 12+3/2 必须表示为 ((1(2+3))/2) 或 ((1*2)+(3/2)) 等等, 这样可以使每一步计算过程清晰明确. 如果设计合理, 还可引入运算符优先级算法, 从而根据用户需求自动确定运算顺序. 然而, 在本文案例中, 我们将简单地强制要求所有复合表达式均使用圆括号以确保计算正确性. 选择使用圆括号还是采用运算符优先级方案具有高度定制性, 这正是自定义脚本语言执行引擎的强大之处
赋值语句
定义一个关键字或操作符,这里采用DEFINE,具体语法为
DEFINE variable_name, expression
代码解释
expression既可以是简单的字面值,也可以是复合表达式
条件语句
条件语句的语法较为复杂。由于涉及较多的分支而显得更为复杂的原因之一在于常见的操作包括多种结构如IF/ ELSE/ ELSE If等以及SWITCH/ CASE WHEN THEN等高级结构。“SWITCH用于处理多个情况”,而“CASE WHEN THEN”则提供了另一种控制流程的方式。“这里仅介绍最常用的三个关键词:IF/ ELSE If/ ELSE。”当需要处理多个分支时,则建议优先选择创建带有Else If结构。
循环语句
在这里描述LOOPI语句的具体使用方法时,默认采用LOOPI语句,并加入两个常用的BREAK和CONTINUE关键字(其中子句的语法类似于PL/SQL语言)。
函数
根据类型划分,存在三种类型的函数:基础功能模块、对象操作工具以及用户自定义功能。其中基础功能模块是语言自身提供的类库中的具体实现方式,在调用时采用访问路径结构的形式(module_name.function_name(args_list))的方式进行调用。这类内置功能通常具有特定领域内的实用性,并且用户对其基本原理和使用方法较为熟悉。而对象操作工具则是由内嵌对象提供的执行操作方式,在使用时需明确具体的调用语法和参数处理规则。最后一种类型是用户自主设计并保存于特定命名空间中的自定义功能模块,在程序开发中可以通过引用该模块名称并结合所需参数进行灵活调用
模块
模块在本质上与命名空间一模一样,并非为了实现某种特殊功能或提供额外价值。它仅仅是为了给每个命名空间中的相同名称函数起一个独特的名字,并从而得以在不同命名空间中明确地区分同一名称的函数。
执行关联文
执行关联文主要承担设定变量作用域和确定函数位置两大功能。
每一个由我们自行编写的自定义函数都会对应一个与其紧密相关的执行关联文。
这一设计取材于JavaScript的设计理念。
每当遇到赋值语句被执行时,
该机制会将指定变量赋值给目标内存单元,
并根据表达式中涉及该变量的情况提取当前内存中的相应值用于计算。
至于为何仅自定义函数拥有独立的执行关联文,
原因在于只有那些由我们自主开发的自定义脚本所编写的自定义函数才具备这种特性,
而其他如对象或内置功能通常是由第三方代码实现的,
例如当我们采用Java作为实现引擎时,
这些功能直接在JVM环境中运行,
并不依赖于我们的脚本引擎进行操作。
演示代码
DEFINE a 1
DEFINE b 2
IF (a GT b) THEN
PRINT(a > b)
ELSIF (a LT b) THEN
PRINT(a < b)
ELSE
PRINT(a = b)
LOOP
DEFINE c (a ADD c)
CONTINUE WHEN (c LT 100)
BREAK WHEN (c EQ 1000)
END LOOP
END IF
IF (1 GT 1) THEN
DEFINE c (a ADD c)
END IF
代码解释
对应的Java引擎解析
package info.woody.visualscript;
import info.woody.visualscript.apicontext.ExecutionContext;
import info.woody.visualscript.apicontext.SubroutineName;
import info.woody.visualscript.core.DnsDebugger;
import info.woody.visualscript.expression.BooleanExpression;
import info.woody.visualscript.token.Assignment;
import info.woody.visualscript.token.If;
import info.woody.visualscript.token.Loop;
import info.woody.visualscript.token.Operation;
import info.woody.visualscript.token.Syntax;
import java.util.List;
public class Sample {
public static void main(String[] args) {
ExecutionContext executionContext = ExecutionContext.newInstance();
executionContext.getStatementList().add(new Assignment("a", "1"));
executionContext.getStatementList().add(new Assignment("b", "2"));
If ifCompareAB = new If(new BooleanExpression("a", Operation.GT, "b"));
ifCompareAB.getStatementList().add((Syntax)executionContext.createSubroutineCall(SubroutineName.PRINT, "a > b"));
ifCompareAB.createElseIfBranch(new BooleanExpression("a", Operation.LT, "b")).getStatementList().add((Syntax)executionContext.createSubroutineCall(SubroutineName.PRINT, "a < b"));;
List<Syntax> elseBranchStatementList = ifCompareAB.createElseBranch().getStatementList();
elseBranchStatementList.add((Syntax)executionContext.createSubroutineCall(SubroutineName.PRINT, "a = b"));
executionContext.getStatementList().add(ifCompareAB);
Loop loop = executionContext.createLoopStatement();
loop.getStatementList().add(executionContext.createAssignmentStatement("c", new BooleanExpression("a", Operation.ADD, "c")));
loop.getStatementList().add(executionContext.createContinueStatement(new BooleanExpression("c", Operation.LT, "100")));
loop.getStatementList().add(executionContext.createBreakStatement(new BooleanExpression("c", Operation.EQ, "1000")));
elseBranchStatementList.add(loop);
If lastIf = executionContext.createIfStatement(new BooleanExpression("c", Operation.GT, "a"));
lastIf.getStatementList().add(executionContext.createAssignmentStatement("c", new BooleanExpression("a", Operation.ADD, "c")));
executionContext.getStatementList().add(lastIf);
executionContext.generateDiagramNavigationScript();
DnsDebugger.debug(executionContext);
}
}
代码解释
实现分析
基于前面所述的语法元素描述内容,请指导我们创建相应的表现形式方案。一旦用户提供脚本时,请指导我们开发一个解析模块来分析其结构特征,并将分析结果转化为上述所示的Java代码结构。随后,在虚拟机(JVM)中运行这些生成的对象。整个流程包括以下几个步骤:首先由用户输入脚本内容;随后由解析器将其转换为中间态表示;接着将该中间态表示进一步转化为可执行的对象;最后在虚拟机(JVM)中运行这些生成的对象。当我们启动虚拟机(JVM)进行程序运行时,在这个过程中我们可以设置断点位置;此时系统会打开一个观察界面让用户实时查看程序运行的状态。
