文章目录
  1. 1. 选择生成什么
    1. 1.1. 嵌套条件
    2. 1.2. 状态表
  2. 2. 如何生成
  3. 3. 混合生成代码和手写代码
  4. 4. 生成可读的代码
  5. 5. 解析之前的代码生成

我们已经谈论过DSL实现组装语义模型来解析DSL文本,很多情况下,一旦语义模型组织完毕,工作就结束了。但是有时候我们没办法这么做,比如,DSL的逻辑只能在很大差异的环境中执行,在那种环境里,语义模型或解析器都极难构建,甚至无法构建。这种时候,就需要代码生成啦。通过代码生成,几乎可以在任何环境下运行DSL指定的行为。

使用代码生成,需要考虑两种不同的环境:DSL处理器和目标环境。DSL处理器是解析器、语义模型和代码生成器之所在,它应该是便于开发的。目标环境则是为生成代码及其周边准备的。使用代码生成的意义在于,将目标环境同DSL处理器分离开来,因为在目标环境下,可能无法构建DSL处理器。

目标环境的限制并不是生成代码的唯一原因。另一个可能的原因是对目标平台缺乏了解。用熟悉的语言指定行为,然后生成不那么熟悉的代码,这样做会容易一些。生成代码还有一个原因,可以更好地强制实施静态检查。用DSL可以描述一些系统的接口,但是系统的其它部分希望用C#与这个接口通信。这种情况下,生成C# API,这样就可以获得编译时检查和IDE支持。

选择生成什么

生成代码时要决定的第一件事是,生成何种代码。可用的代码生成的风格有:“基于模型的代码生成”和“无视模型的代码生成”。二者的差别在于,在目标环境中,“语义模型”是否显式地表现出来。

我们以状态机为例,实现状态机有两种经典方案,分别是嵌套条件和状态表。

嵌套条件

public void handle(Event event) {
    switch(currentState) {
        case ON: switch (event) {
                    case DOWN:
                        currentState = OFF;
                    }
        case DOWN: switch (event) {
                    case UP:
                        currentState = ON;
                    }
    }
}

这是无视模型的代码生成,因为状态机的逻辑嵌套在语言的控制流中—语义模型没有显示地表现出来。

状态表

class ModelMachine..
    private State currentState;
    private Map<State, Map<Event, State>> states = new HashMap<State, Map<Event, State>>();

    public ModelMachine(State currentSate) {
        this.currentState = currentState;
    }
    void defineTransition(State source, Event trigger, State target) {
        if(!state.containsKey(source)) states.put(source, new HashMap<Event, State>);
        states.get(source).put(trigger, target);
    }

    public void handle(Event event) {
        Map<Event, State> currentTransitions = states.get(currentState);
        if(null == currentTransitions) return;
        State target = currentTransitions.get(event);
        if(null != target) currentState = target;
    }
}

这是基于模型的代码生成。代码完全通用,需要使用专用的代码进行配置才能运作起来。

modelMachine = new ModelMachine(OFF);
modelMachine.defineTransition(OFF, UP, ON);
modelMachine.defineTransition(ON, DOWN, OFF);

通过将语义模型表现在生成代码中,通用代码框架代码和专用配置代码分离开来。基于模型的代码生成保留了通用/专用的区分,而无视模型的代码生成则把语义模型放入控制流中,将二者混在一起。

如何生成

一旦想好了生成何种代码,接下来就要考虑生成过程了。如果生成文本输出的话,有两种主要的风格可以遵循:“基于转换器的代码生成”和“模板化的生成器”。

基于转换器的代码生成例子,要编写代码,读取语义模型,生成目标源码的语句。

模板化的生成器,会写一个实例的输出文件。输出文件会有特定状态机专有的一些东西,在这样的地方,会放置一些特殊模版标记,这样就可以调用语义模型生成相应的代码。

模板化的生成器是由输出的结构驱动的,而基于转换器的代码生成则是由输入、输出或者二者共同驱动的。

二者用起来都不错,模板化的生成器适用于输出里有大量静态代码的情况。否则,更倾向于使用基于转换器的代码生成。

使用模版化的生成器最大的问题是生成可变输出会用到宿主代码,情况不妙,就会淹没静态模版的代码。

混合生成代码和手写代码

有时,在目标环境下执行的所有代码都可以生成,但更常见的是,需要混合生成代码和手写代码。这时需要遵守一些规则:

  • 不要修改生成代码
  • 将生成代码和手写代码严格分开

生成可读的代码

虽然生成的代码无法手动修改,但是我们仍然尽量要生成可读性良好的代码,除非要花额外时间才能得到一个正确的结构。

解析之前的代码生成

在某些情况下,DSL、脚本要集成一些外部信息。可以用代码生成生成脚本编写所需的信息。这样在组装语义模型时,就可以利用这些信息进行检测。

文章目录
  1. 1. 选择生成什么
    1. 1.1. 嵌套条件
    2. 1.2. 状态表
  2. 2. 如何生成
  3. 3. 混合生成代码和手写代码
  4. 4. 生成可读的代码
  5. 5. 解析之前的代码生成