从零写一个编译器(十):编译前传之直接解释执行

项目的完整代码在 C2j-Compiler

前言

这一篇不看也不会影响后面代码生成部分

现在经过词法分析语法分析语义分析,终于可以进入最核心的部分了。前面那部分可以称作编译器的前端,代码生成代码优化都是属于编译器后端,如今有关编译器的工作岗位主要都是对后端的研究。当然现在写的这个编译器因为水平有限,并没有优化部分。

在进行代码生成部分之前,我们先来根据AST来直接解释执行,其实就是对AST的遍历。现代解释器一般都是生成一个比较低级的指令然后跑在虚拟机上,但是简单起见我们就直接根据AST解释执行的解释器。(原本这部分是不想写的,是可以直接写代码生成的)

这次的文件在interpreter包里,这次涉及到的文件比较多,就不列举了

一个小问题

在开始说解释器的部分前我们看一下,认真观察之前在构造符号表对赋初值的推导式的处理是有问题的,但是问题不大,只要稍微改动一下

在github源代码的部分已经改了,改动如下:

case SyntaxProductionInit.VarDecl_Equal_Initializer_TO_Decl:
    attributeForParentNode = (Symbol) valueStack.get(valueStack.size() - 3);
    ((Symbol) attributeForParentNode).value = initialValue;
break;

case SyntaxProductionInit.Expr_TO_Initializer:
    initialValue = (Integer) valueStack.get(valueStack.size() - 1);
    System.out.println(initialValue);
    break;

其实就是一个拿到赋的初值放到Symbol的value里

Executor接口

所有能够执行结点的类都要实现这个接口,所以以此来达到遍历AST来执行代码

解释器的启动在Interpreter类里,它也实现了Executor接口

Interpreter类的execute传入的参数就是整棵抽象语法树的头节点了,ExecutorFactory的getExecutor则是根据当前结点的TokenType返回一个可以解释当前节点的类,而其它执行节点的类都继承了BaseExecutor

@Override
public Object execute(AstNode root) {
    if (root == null) {
        return null;
    }

    ExecutorFactory factory = ExecutorFactory.getInstance();
    Executor executor = factory.getExecutor(root);
    executor.execute(root);

    return root;
}

BaseExecutor的两个主要方法就是执行它的子节点,并且可以指定执行哪个子节点。可以先忽略Brocaster,这些是用来实现执行节点类之前的通讯的,现在还没有用。reverseChildren是用来对节点的反转,因为在创建的AST的过程由于堆栈的原因,所以节点顺序的相反的。continueExecute是标志位,后面可能会执行到设置它的节点来结束运行

protected void executeChildren(AstNode root) {
    ExecutorFactory factory = ExecutorFactory.getInstance();
    root.reverseChildren();

    int i = 0;
    while (i < root.getChildren().size()) {
        if (!continueExecute) {
            break;
        }

        AstNode child = root.getChildren().get(i);
        executorBrocaster.brocastBeforeExecution(child);
        Executor executor = factory.getExecutor(child);
        if (executor != null) {
            executor.execute(child);
        } else {
            System.err.println("Not suitable Generate found, node is: " + child.toString());
        }

        executorBrocaster.brocastAfterExecution(child);

        i++;
    }
}

protected AstNode executeChild(AstNode root, int childIdx) {
    root.reverseChildren();
    AstNode child;
    ExecutorFactory factory = ExecutorFactory.getInstance();
    child = (AstNode)root.getChildren().get(childIdx);
    Executor executor = factory.getExecutor(child);
    AstNode res = (AstNode)executor.execute(child);

    return res;
}

解释执行

我们可以知道一个C语言的源文件一般都是一些函数定义和一个main的函数来启动,所以在AstBuilder里返回给Interpreter的节点就是从main开始的

public AstNode getSyntaxTreeRoot() {
    AstNode mainNode = funcMap.get("main");
    return mainNode;
}

执行函数ExtDefExecutor

用来执行函数的Executor是ExtDefExecutor

  • 在进入execute会先执行FunctDecl节点,再执行CompoundStmt节点
  • saveArgs和restoreArgs属于保护当前的环境,就是进入其它作用域的时候保证这个符号不变修改,不比如当作参数传递的时候
  • returnVal也是属于由其它节点设置的属性
  • root.setAttribute的作用就是对节点设置属性,把值往上传递
@Override
public Object execute(AstNode root) {
    this.root = root;
    int production = (Integer) root.getAttribute(NodeKey.PRODUCTION);
    switch (production) {
        case SyntaxProductionInit.OptSpecifiers_FunctDecl_CompoundStmt_TO_ExtDef:
            AstNode child = root.getChildren().get(0);
            funcName = (String) child.getAttribute(NodeKey.TEXT);
            root.setAttribute(NodeKey.TEXT, funcName);
            saveArgs();
            executeChild(root, 0);

            executeChild(root, 1);
            Object returnVal = getReturnObj();
            clearReturnObj();

            if (returnVal != null) {
                root.setAttribute(NodeKey.VALUE, returnVal);
            }
            isContinueExecution(true);
            restoreArgs();
            break;

        default:
            break;
    }
    return root;
}

函数定义 FunctDeclExecutor

执行函数会先执行它的括号的前部分也就是标识符和参数那部分,对参数进行初始化,函数的传递的参数用单独一个类FunctionArgumentList来表示

@Override
public Object execute(AstNode root) {
    int production = (Integer) root.getAttribute(NodeKey.PRODUCTION);
    Symbol symbol;
    currentNode = root;

    switch (production) {
        case SyntaxProductionInit.NewName_LP_RP_TO_FunctDecl:
            root.reverseChildren();
            copyChild(root, root.getChildren().get(0));
            break;

        case SyntaxProductionInit.NewName_LP_VarList_RP_TO_FunctDecl:
            symbol = (Symbol) root.getAttribute(NodeKey.SYMBOL);

            Symbol args = symbol.getArgList();
            initArgumentList(args);

            if (args == null || argsList == null || argsList.isEmpty()) {
                System.err.println("generate function with arg list but arg list is null");
                System.exit(1);
            }
            break;

        default:
            break;
    }

    return root;
}

执行语句部分 CompoundStmtExecutor

执行语句的部分就开始对树的遍历执行,但是我们来看一下这个节点的推导式

COMPOUND_STMT-> LC LOCAL_DEFS STMT_LIST RC

在构建AST的时候我们并没有构建LOCAL_DEFS,并且在之前符号表也没有进行处理,所以我们直接执行第0个节点就可以了

@Override
public Object execute(AstNode root) {
    return executeChild(root, 0);
}

一元操作

下面看UnaryNodeExecutor,UnaryNodeExecutor应该是所有Executor最复杂的之一了,其实对于节点执行,先执行子节点,并且向上传递执行结果的值。

只说其中的几个

  • 指针

这个就是对指针的操作了,本质是对内存分配的一个模拟,再设置实现ValueSetter的DirectMemValueSetter,让它的父节点可以通过这个节点的setter对指针指向进行赋值

ValueSetter是一个可以对变量进行赋值的接口,数组、指针、简单的变量都有各自的valueSetter

case SyntaxProductionInit.Start_Unary_TO_Unary:
    child = root.getChildren().get(0);
    int addr = (Integer) child.getAttribute(NodeKey.VALUE);
    symbol = (Symbol) child.getAttribute(NodeKey.SYMBOL);

    MemoryHeap memHeap = MemoryHeap.getInstance();
    Map.Entry<Integer, byte[]> entry = memHeap.getMem(addr);
    int offset = addr - entry.getKey();
    if (entry != null) {
        byte[] memByte = entry.getValue();
        root.setAttribute(NodeKey.VALUE, memByte[offset]);
    }

    DirectMemValueSetter directMemSetter = new DirectMemValueSetter(addr);
    root.setAttribute(NodeKey.SYMBOL, directMemSetter);
    break;
  • 指针和数组操作:

这是执行数组或者是指针的操作,对于数组和指针的操作会在节点中的Symbol里设置一个可以进行赋值的接口:ArrayValueSetter、PointerValueSetter,逻辑都不是很复杂。对于指针的操作其实是对于内存地址分配的一个模拟。

case SyntaxProductionInit.Unary_LB_Expr_RB_TO_Unary:
    child = root.getChildren().get(0);
    symbol = (Symbol) child.getAttribute(NodeKey.SYMBOL);

    child = root.getChildren().get(1);
    int index = (Integer) child.getAttribute(NodeKey.VALUE);

    try {
        Declarator declarator = symbol.getDeclarator(Declarator.ARRAY);
        if (declarator != null) {
            Object val = declarator.getElement(index);
            root.setAttribute(NodeKey.VALUE, val);
            ArrayValueSetter setter = new ArrayValueSetter(symbol, index);
            root.setAttribute(NodeKey.SYMBOL, setter);
            root.setAttribute(NodeKey.TEXT, symbol.getName());
        }
        Declarator pointer = symbol.getDeclarator(Declarator.POINTER);
        if (pointer != null) {
            setPointerValue(root, symbol, index);

            PointerValueSetter pv = new PointerValueSetter(symbol, index);
            root.setAttribute(NodeKey.SYMBOL, pv);
            root.setAttribute(NodeKey.TEXT, symbol.getName());
        }

    } catch (Exception e) {
        System.err.println(e.getMessage());
        e.printStackTrace();
        System.exit(1);
    }
    break;
  • 函数调用

函数调用也是属于一元操作,对于函数调用有两种情况:一种是自定义的函数,还有一种是解释器提供的函数

  1. 如果是自定义函数,就找到这个函数的头节点,从这个头节点开始执行
  2. 如果是解释器提供的函数,就交由ClibCall处理,比如printf就是属于库函数
case SyntaxProductionInit.Unary_LP_RP_TO_Unary:
case SyntaxProductionInit.Unary_LP_ARGS_RP_TO_Unary:
    String funcName = (String) root.getChildren().get(0).getAttribute(NodeKey.TEXT);
    if (production == SyntaxProductionInit.Unary_LP_ARGS_RP_TO_Unary) {
        AstNode argsNode = root.getChildren().get(1);
        ArrayList<Object> argList = (ArrayList<Object>) argsNode.getAttribute(NodeKey.VALUE);
        ArrayList<Object> symList = (ArrayList<Object>) argsNode.getAttribute(NodeKey.SYMBOL);
        FunctionArgumentList.getInstance().setFuncArgList(argList);
        FunctionArgumentList.getInstance().setFuncArgSymbolList(symList);
    }

    AstNode func = AstBuilder.getInstance().getFunctionNodeByName(funcName);
    if (func != null) {
        Executor executor = ExecutorFactory.getInstance().getExecutor(func);
        executor.execute(func);
        Object returnVal = func.getAttribute(NodeKey.VALUE);
        if (returnVal != null) {
            ConsoleDebugColor.outlnPurple("function call with name " + funcName + " has return value that is " + returnVal.toString());
            root.setAttribute(NodeKey.VALUE, returnVal);
        }
    } else {
        ClibCall libCall = ClibCall.getInstance();
        if (libCall.isApiCall(funcName)) {
            Object obj = libCall.invokeApi(funcName);
            root.setAttribute(NodeKey.VALUE, obj);
        }
    }
    break;

逻辑语句处理

逻辑语句处理无非就是根据节点值判断该执行哪些节点

  • FOR、WHILE语句

代码逻辑和语句的逻辑是一样,比如对于

for(i = 0; i < 5; i++){}

就会先执行i = 0部分,在执行{}和i++部分,然后再判断条件是否符合

case SyntaxProductionInit.FOR_OptExpr_Test_EndOptExpr_Statement_TO_Statement:
executeChild(root, 0);

while (isLoopContinute(root, LoopType.FOR)) {
    //execute statement in for body
    executeChild(root, 3);
    //execute EndOptExpr
    executeChild(root, 2);
}
break;

case SyntaxProductionInit.While_LP_Test_Rp_TO_Statement:
while (isLoopContinute(root, LoopType.WHILE)) {
    executeChild(root, 1);
}
break;
  • IF语句

if语句就是先执行判断部分,再根据判断的结果来决定是否执行{}块

@Override
public Object execute(AstNode root) {

    AstNode res = executeChild(root, 0);
    Integer val = (Integer)res.getAttribute(NodeKey.VALUE);
    copyChild(root, res);

    if (val != null && val != 0) {
        executeChild(root, 1);
    }

    return root;
}

小结

这一篇写的很乱,一是解释器部分还是蛮大的,想在一篇之内写完比较难。所以省略了很多东西。但其实对于解释器实现部分对于AST的遍历才比较涉及编译原理部分,其它的主要是逻辑实现

对于解释器部分,因为没有采用虚拟机那样的实现,而是直接对AST的遍历。所以对AST的遍历是关键,主要在于遍历到该执行的子节点部分,然后处理逻辑,再把信息通过子节点传递到父节点部分。

原文地址:https://www.cnblogs.com/secoding/p/11382017.html

时间: 2024-10-07 07:56:59

从零写一个编译器(十):编译前传之直接解释执行的相关文章

从零写一个编译器(三):语法分析之几个基础数据结构

项目的完整代码在 C2j-Compiler 写在前面 这个系列算作为我自己在学习写一个编译器的过程的一些记录,算法之类的都没有记录原理性的东西,想知道原理的在龙书里都写得非常清楚,但是我自己一开始是不怎么看得下来,到现在都还没有完整的看完,它像是一本给已经有基础的人写的书. 在parse包里一共有8个文件,就是语法分析阶段写的所有东西啦 Symbols.java Production.java SyntaxProductionInit.java FirstSetBuilder.java Prod

从零写一个编译器(五):语法分析之自动机的缺陷和改进

项目的完整代码在 C2j-Compiler 前言 在上一篇,已经成功的构建了有限状态自动机,但是这个自动机还存在两个问题: 无法处理shift/reduce矛盾 状态节点太多,导致自动机过大,效率较低 这一节就要解决这两个问题 shift/reduce矛盾 看上一节那个例子的一个节点 e -> t . t -> t . * f 这时候通过状态节点0输入t跳转到这个节点,但是这时候状态机无法分清是根据推导式1做reduce还是根据推导式2做shift操作,这种情况就称之为shift / redu

从零写一个编译器(七):语义分析之符号表的数据结构

项目的完整代码在 C2j-Compiler 前言 有关符号表的文件都在symboltable包里 前面我们通过完成一个LALR(1)有限状态自动机和一个reduce信息来构建了一个语法解析表,正式完成了C语言的语法解析.接下来就是进入语义分析部分,和在第二篇提到的一样,语义分析的主要任务就是生成符号表来记录变量和变量的类型,并且发现不符合语义的语句 描述变量 在C语言里对变量声明定义里,主要有两种描述 说明符(Specifier) 说明符也就是对应C语言的一些描述变量类型或者像static,ex

从零写一个编译器(十三):代码生成之遍历AST

项目的完整代码在 C2j-Compiler 前言 在上一篇完成对JVM指令的生成,下面就可以真正进入代码生成部分了.通常现代编译器都是先把生成IR,再经过代码优化等等,最后才编译成目标平台代码.但是时间水平有限,我们没有IR也没有代码优化,就直接利用AST生成Java字节码 入口 进行代码生成的入口在CodeGen,和之前解释器一样:先获取main函数的头节点,从这个节点开始,先进入函数定义,再进入代码块 函数定义节点 在进入函数定义节点的时候,就要生成一个函数定义对应的Java字节码,即一个静

从零写一个编译器(六):语法分析之表驱动语法分析

项目的完整代码在 C2j-Compiler 前言 上一篇已经正式的完成了有限状态自动机的构建和足够判断reduce的信息,接下来的任务就是根据这个有限状态自动机来完成语法分析表和根据这个表来实现语法分析 reduce信息 在完成语法分析表之前,还差最后一个任务,那就是描述reduce信息,来指导自动机是否该进行reduce操作 reduce信息在ProductionsStateNode各自的节点里完成,只要遍历节点里的产生式,如果符号"."位于表达式的末尾,那么该节点即可根据该表达式以

从零写一个编译器(十一):代码生成之Java字节码基础

项目的完整代码在 C2j-Compiler 前言 第十一篇,终于要进入代码生成部分了,但是但是在此之前,因为我们要做的是C语言到字节码的编译,所以自然要了解一些字节码,但是由于C语言比较简单,所以只需要了解一些字节码基础 JVM的基本机制 JVM有一个执行环境叫做stack frame 这个环境有两个基本数据结构 执行堆栈:指令的执行,都会围绕这个堆栈来进行 局部变量数组,参数和局部变量就存储在这个数组. 还有一个PC指针,它指向下一条要执行的指令. 举一个例子 int f(int a, int

从零写一个编译器(四):语法分析之构造有限状态自动机

项目的完整代码在 C2j-Compiler 通过上一篇对几个构造自动机的基础数据结构的描述,现在就可以正式来构造有限状态自动机 我们先用一个小一点的语法推导式来描述这个过程 s -> e e -> e + t e -> t t -> t * f t -> f f -> ( e ) f -> NUM 初始化 状态0是状态机的初始状态,它包含着语法表达式中的起始表达式,也就是编号为0的表达式: 0: s -> . e 这里的点也就是之前Production类中的

从零写一个编译器(二):语法分析之前置知识

前言 在之前完成了词法分析之后,得到了Token流,那么接下来就是实现语法分析器来输入Token流得到抽象语法树 (Abstract Syntax Tree,AST).但是在完成这个语法分析器不像词法分析器,直接手撸就好了,还是需要一些前置的知识. 这些前置知识在之前的博文都有提起过 之前的博文目录 项目的完整代码在 C2j-Compiler 什么是语法分析? 如果我们把词法分析看成是组合单词,输出单词流,那么语法分析就可以看作是检查这些单词是不是符合语法的过程.在词法分析的时候用正则或者手工比

如何写一个解释器(1):编译原理

最近在看DSL的东西,对于外部DSL,写一个解释器是必不可少的.我试图归纳一下我学到的,以写一个解释器为目标,讲一下如果来实现一个可用的解释器.一个解释器通常可以分为一下几个阶段: 词法分析(Lexer) 语法分析(Parser, BNF, CFG, AST) 语义分析(AST的处理, annotated AST) 目标语言生成(stack-based) 这里的解释器不包括目标语言的执行和运行时环境,如果需要类似于python/ruby的解析执行器的话,还需要bytecode-compiler,