更详细的讲解和代码调试演示过程,请参看视频
用java开发C语言编译器

上一节,我们实现了没有参数传递的函数调用,本节,我们看看如何实现有参数传递的函数调用。

有参数的函数调用要比无参数的函数调用复杂的多,一个难题在于,我们需要确定参数变量的作用域,例如下面的代码:

int a;

void f(int a, int b) {
    int c;
    c = a + b;
}

在代码里,有两个同名变量都成为a, 显然,这两个变量作用范围不同,他们的存在并不矛盾。当两个变量存在符号表中时,由于名字相同,要实现参数传递时,必须确定参数的值传递给正确的变量a, 如果传递出错了,那么整个程序运行的逻辑就混乱了。

因此,要实现有参数的函数调用,首要问题是确保把数值传递给对应左右域的相应变量。那么,如何确定变量的作用域呢。根据我们的代码实现,每个变量都对应一个Symbol对象,我们在该对象里添加一个字符串变量,用来表明该对象对应变量的作用域,如果变量是全局变量,那么它的作用域字符串内容为”global”,如果变量是某个函数的参数,或是定义在函数体内,那么该变量的作用域字符串就可以用该函数的名字来定义。

以上面代码为例,第一个变量a,作用域字符串是”global”, 第二个变量a,作用域范围是”f”, 也就是它对应的函数名字。我们看看代码如何实现这个功能。

一条变量定义的代码语句,例如:
int a;
对应的语法表达式为:
EXT_DEF -> OPT_SPECIFIERS EXT_DECL_LIST SEMI

因此,当语法解析器读入一条语句,然后解析成上面的语句时,我们就知道,当前代码正在定义一个变量,此时就可以设置该变量的作用域字符串了,代码如下:

private void takeActionForReduce(int productNum) {
...
case CGrammarInitializer.OptSpecifier_ExtDeclList_Semi_TO_ExtDef:
        case CGrammarInitializer.TypeNT_VarDecl_TO_ParamDeclaration:
        case CGrammarInitializer.Specifiers_DeclList_Semi_TO_Def:
            Symbol symbol = (Symbol)attributeForParentNode;
            TypeLink specifier = (TypeLink)(valueStack.get(valueStack.size() - 3));
            typeSystem.addSpecifierToDeclaration(specifier, symbol);
            typeSystem.addSymbolsToTable(symbol, symbolScope);
...
}

我们早在语法解析是讲解过上面代码,当C语言定义一个变量时,上面对应的case代码会被执行,进而为生成的变量对应的Symbol对象,在把Symbol对象加入符号表时,需要多添加一个变量,就是symbolScope,这是一个字符串全局变量,用来代表当前变量所在的作用域,它的初始化方式如下:

public class LRStateTableParser {
    private Lexer lexer;
    int    lexerInput = 0;
    int    nestingLevel = 0;
    int    enumVal = 0;
    String text = "";
    public static final String GLOBAL_SCOPE = "global";
    public String symbolScope = GLOBAL_SCOPE;
    ...
    }

它一开始时就被初始化为”global”,因此,我们的解析器在遇到变量声明时,先把当前变量设置为global的,如果在后面的解析中,发现该变量并不是全局变量,那么在后面再修改该变量的作用范围。例如函数调用:

void f(int a, int b) {
    int c;
    c = a + b;
}

两个参数a,b,在第一次解析时,根据前面的case代码,会先为这两个变量的作用域字符串设置为”global”,然后继续解析,等到解析器解析完整个函数头,也就是当解释器根据以下表达式进行递归时:

FUNCT_DECL -> NEW_NAME LP VAR_LIST RP
FUNCT_DECL -> NEW_NAME LP RP

此时,我们可以得知,当前解析器解析的代码是函数头部定义,这个时候,我们将改变symbolScope的内容,把它变成当前解析函数的函数名,这样,接下来遇到变量声明时,它对应的作用域字符串就会变成当前的函数名。

前面我们说过,参数定义时,解析器首先会把它的作用域设置为global,即使它是函数的参数,现在我们解析到函数头定义了,这时候是把原来参数的作用域范围更改为对应函数的正确时机,因此代码如下:

private void takeActionForReduce(int productNum) {
   ....
case CGrammarInitializer.NewName_LP_VarList_RP_TO_FunctDecl:
            setFunctionSymbol(true);
            Symbol argList = (Symbol)valueStack.get(valueStack.size() - 2);
            ((Symbol)attributeForParentNode).args = argList;
            typeSystem.addSymbolsToTable((Symbol)attributeForParentNode, symbolScope);
            //遇到函数定义,变量的scope名称要改为函数名,并把函数参数的scope改为函数名
            symbolScope = ((Symbol)attributeForParentNode).getName();
            Symbol sym = argList;
            while (sym != null) {
                sym.addScope(symbolScope);
                sym = sym.getNextSymbol();
            }
            break;

        case CGrammarInitializer.NewName_LP_RP_TO_FunctDecl:
            setFunctionSymbol(false);
            typeSystem.addSymbolsToTable((Symbol)attributeForParentNode, symbolScope);
            //遇到函数定义,变量的scope名称要改为函数名
            symbolScope = ((Symbol)attributeForParentNode).getName();

            break;
   ....
}

从上面代码我们可以看到,在进入到函数头的解析时,解释器会把symbolScope设置为函数名,如果当前的case 等于CGrammarInitializer.NewName_LP_VarList_RP_TO_FunctDecl时,通过argList变量获得函数参数对应的输入参数变量链表,这个链表的建立,我们在前面章节中已经详细解释过。然后通过参数链表变量每个参数,把参数的作用域字符串改为对应的函数名。

等到函数全部解析完毕后,变量的作用域就得重新转变为global, 根据前一节内容,我们知道,函数定义解析完毕对应的语法表达式为:
EXT_DEF -> OPT_SPECIFIERS FUNCT_DECL COMPOUND_STMT

当解释器根据上面的表达式进行递归时,我们知道,当前状态是函数解析结束,因此,我们在这时就需要把symbolScope的内容,重新改为global.代码如下:

 private void takeActionForReduce(int productNum) {
        switch(productNum) {
        ...
        case CGrammarInitializer.OptSpecifiers_FunctDecl_CompoundStmt_TO_ExtDef:
            symbol = (Symbol)valueStack.get(valueStack.size() - 2);
            specifier = (TypeLink)(valueStack.get(valueStack.size() - 3));
            typeSystem.addSpecifierToDeclaration(specifier, symbol);

            //函数定义结束后,接下来的变量作用范围应该改为global
            symbolScope = GLOBAL_SCOPE;
            break;
        ...
        }

请大家通过视频查看代码的讲解和调试过程,以便获得更详细的理解。

接下来,我们看看函数是如何传递的。上一节,我们知道,没有参数输入的函数调用对应的语法表达式是:

UNARY -> UNARY LP RP

那么如果,函数调用有参数的话,其对应的语法表达式是:

UNARY -> UNARY LP ARGS RP
ARGS -> NO_COMMA_EXPR
ARGS -> NO_COMMA_EXPR COMMA ARGS

由此,我们需要构造一个ARGS对应的节点,代码如下:

public ICodeNode buildCodeTree(int production, String text) {
        ICodeNode node = null;
        Symbol symbol = null;

        switch (production) {
        ...
        case CGrammarInitializer.NoCommaExpr_TO_Args:
            node = ICodeFactory.createICodeNode(CTokenType.ARGS);
            node.addChild(codeNodeStack.pop());
            break;

        case CGrammarInitializer.NoCommaExpr_Comma_Args_TO_Args:
            node = ICodeFactory.createICodeNode(CTokenType.ARGS);
            node.addChild(codeNodeStack.pop());
            node.addChild(codeNodeStack.pop());
            break;
        ...
        }

由此,我们还需要构造一个ARGS节点对应的Executor对象,以便实现参数解析,代码如下:

package backend;

import java.util.ArrayList;

import frontend.CGrammarInitializer;

public class ArgsExecutor extends BaseExecutor {

    @Override
    public Object Execute(ICodeNode root) {
        int production = (Integer)root.getAttribute(ICodeKey.PRODUCTION);
        ArrayList<Object> argList = new ArrayList<Object>();
        ICodeNode child ;
        switch (production) {
        case CGrammarInitializer.NoCommaExpr_TO_Args:
            child = (ICodeNode)executeChild(root, 0);
            int val = (Integer)child.getAttribute(ICodeKey.VALUE);
            argList.add(val);
            break;

        case CGrammarInitializer.NoCommaExpr_Comma_Args_TO_Args:
            child = executeChild(root, 0);
            val = (Integer)child.getAttribute(ICodeKey.VALUE);
            argList.add(val);

            child = (ICodeNode)executeChild(root, 1);
            ArrayList<Object> list = (ArrayList<Object>)child.getAttribute(ICodeKey.VALUE);
            argList.addAll(list);
            break;
        }

        root.setAttribute(ICodeKey.VALUE, argList);
        return root;
    }

}

对应函数调用,例如f(1,2,3) ,ArgsExecutor 的作用是构造一个参数队列:
3 -> 2 -> 1, 然后把这个队列交给函数的执行对象,也就是ExtDefExecutor.
ArgsExecutor 先通过子执行子节点,把数字字符串读取成对应的数值,然后再把这些数值加入一个队列中返回。

同时UnaryExecutor也要做相应变化,它需要让ArgsExecutor执行后,获取参数的数值列表,以便传递给ExtDefExecutor,对应的代码改动如下:

public class UnaryNodeExecutor extends BaseExecutor{

    @Override
    public Object Execute(ICodeNode root) {
        executeChildren(root);

        int production = (Integer)root.getAttribute(ICodeKey.PRODUCTION); 
        String text ;
        Symbol symbol;
        Object value;
        ICodeNode child;

        switch (production) {
        ...
        case CGrammarInitializer.Unary_LP_RP_TO_Unary:
        case CGrammarInitializer.Unary_LP_ARGS_RP_TO_Unary:
            //先获得函数名
            String funcName = (String)root.getChildren().get(0).getAttribute(ICodeKey.TEXT);
            if (production == CGrammarInitializer.Unary_LP_ARGS_RP_TO_Unary) {
                ICodeNode argsNode = root.getChildren().get(1);
                ArrayList<Object> argList = (ArrayList<Object>)argsNode.getAttribute(ICodeKey.VALUE);
                FunctionArgumentList.getFunctionArgumentList().setFuncArgList(argList); 
            }

            //找到函数执行树头节点
            ICodeNode func = CodeTreeBuilder.getCodeTreeBuilder().getFunctionNodeByName(funcName);
            if (func != null) {
                Executor executor = ExecutorFactory.getExecutorFactory().getExecutor(func);

                executor.Execute(func);
            }
            break;
        ...
        }

executeChildren(root);首先让孩子节点先执行,由于ARGS是UNARY的孩子节点,所以executeChildren会让ArgsExecutor先执行,这样就可以获取参数数值列表。然后通过ARGS节点得到参数列表,也就是执行下面语句得到参数列表:

ICodeNode argsNode = root.getChildren().get(1);
                ArrayList<Object> argList = (ArrayList<Object>)argsNode.getAttribute(ICodeKey.VALUE);

有了列表之后,怎么把列表传递给函数执行体呢,是通过一个单子对象存储的:

package backend;

import java.util.ArrayList;

public class FunctionArgumentList {
    private static FunctionArgumentList argumentList = null;
    private ArrayList<Object> funcArgList = new ArrayList<Object>();

    public static FunctionArgumentList getFunctionArgumentList() {
        if (argumentList == null) {
            argumentList = new FunctionArgumentList();
        }

        return argumentList;
    }

    public void setFuncArgList(ArrayList<Object> list) {
        funcArgList = list;
    }

    public ArrayList<Object> getFuncArgList() {
        return funcArgList;
    }

    private FunctionArgumentList() {}
}

UnaryExecutor将获得的列表放入FunctionArgumentList对象,然后从根据要调用的函数名,从函数哈希表中找到函数执行树的头结点,接着再通过ExtDefExecutor去执行函数体内的语句。

有了参数列表,接下来要做的是把参数列表对应的数值传递给参数,这样函数运行时才能获得输入的数值,数值传递是由FunctDeclExecutor实现的,代码如下:

public class FunctDeclExecutor extends BaseExecutor {
    private ArrayList<Object> argsList = null;
    private ICodeNode currentNode;
    @Override
    public Object Execute(ICodeNode root) {
    switch (production) {
    ...
    case  CGrammarInitializer.NewName_LP_VarList_RP_TO_FunctDecl:
            symbol = (Symbol)root.getAttribute(ICodeKey.SYMBOL);
            //获得参数列表
            Symbol args = symbol.getArgList();
            initArgumentList(args);

            if (args == null || argsList == null || argsList.isEmpty()) {
                //如果参数为空,那就是解析错误
                System.err.println("Execute function with arg list but arg list is null");
                System.exit(1);
            }

            break;
    ...
    }
private void initArgumentList(Symbol args) {
        if (args == null) {
            return;
        }


        argsList = FunctionArgumentList.getFunctionArgumentList().getFuncArgList();
        Collections.reverse(argsList);
        Symbol eachSym = args;
        int count = 0;
        while (eachSym != null) {
            IValueSetter setter = (IValueSetter)eachSym;
            try {
                /*
                 * 将每个输入参数设置为对应值并加入符号表
                 */
                setter.setValue(argsList.get(count));
                count++;
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            eachSym = eachSym.getNextSymbol();
        }
    }

}

它先通过FunctionArgumentList获得参数数值列表,然后找到对应的参数symbol对象列表,逐个把传入数值设置到参数对应的symbol中,当symbol参数数值设置正确后,函数体就能正确执行了

函数体的执行在上一节我们曾经讨论过,再次不再讨论。请通过参看视频获得更详细的代码讲解和调试演示过程,以便增加理解。

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:

更多推荐

java实现C语言编译器:实现有参数的函数调用