美文网首页toy_compiler
编译原理——函数!

编译原理——函数!

作者: madao756 | 来源:发表于2019-10-23 21:16 被阅读0次

前言:函数的实现又是一个难点,我们一点一点攻破!

0X00 函数调用

按照惯例我们写出函数调用的「文法」:

unary → ( "!" | "-" ) unary | call ;
call  → primary ( "(" arguments? ")" )* ;
arguments → expression ( "," expression )* ;

这样的文法可以匹配这样的函数调用:

A();
A()()();
A(a, b);

符合我们的语言。

接着,写出函数调用「抽象语法树」的格式:

static class Call extends Expr {
    Call(Expr callee, Token paren, List<Expr> arguments) {
        this.callee = callee;
        this.paren = paren;
        this.arguments = arguments;
    }

    <R> R accept(Visitor<R> visitor) {
        return visitor.visitCallExpr(this);
    }

    final Expr callee;
    final Token paren;
    final List<Expr> arguments;
}

然后在 parser 中,实现生成抽象语法树的代码:

private Expr unary() {
    // unary → ( "!" | "-" ) unary
    // | primary ;
    if (match(BANG, MINUS)) {
        Token operator = previous();
        Expr right = unary();
        return new Expr.Unary(operator, right);
    }

    return call();
}

private Expr call() {
    // 匹配 primary ( "(" arguments? ")" )* ;
    Expr expr = primary();
    // A() 中 A 的部分叫做 callee
    // A()() 中 A() 的部分也可以叫做 callee
    // 所以我们要,不断的匹配左侧 callee 的部分,并生成抽象语法树
    Expr callee = expr;
    while (true) {
        if (match(LEFT_PAREN)) {
            callee = getCallee(callee);
        } else {
            expr = callee;
            break;
        }
    }

    return expr;
}

private Expr getCallee(Expr callee) {
    // 匹配所有的参数
    List<Expr> arguments = new ArrayList<>();
    if (!check(RIGHT_PAREN)) {
        do {
            if (arguments.size() >= 255) {
                error(peek(), "Cannot have more than 255 arguments.");
            }
            arguments.add(expression());
        } while (match(COMMA));
    }

    Token paren = consume(RIGHT_PAREN, "Expect ')' after arguments.");

    return new Expr.Call(callee, paren, arguments);
}

最后执行这个「抽象语法树」

@Override
public Object visitCallExpr(Expr.Call expr) {
    // 首先执行 callee
    Object callee = evaluate(expr.callee);
    // 计算所有参数
    List<Object> arguments = new ArrayList<>();
    for (Expr argument : expr.arguments) {
        arguments.add(evaluate(argument));
    }
    
    // 如果最后的 primary 是 "" 之类的东西,要报错,不能执行
    if (!(callee instanceof LoxCallable)) {
        throw new RuntimeError(expr.paren, "Can only call functions and classes.");
    }

    // 判断有没有超过最大参数
    LoxCallable function = (LoxCallable) callee;
    if (arguments.size() != function.arity()) {
        throw new RuntimeError(expr.paren,
                               "Expected " + function.arity() + " arguments but got " + arguments.size() + ".");
    }
    
    // 函数如何执行的暂时不表
    return function.call(this, arguments);
}

其中 LoxCallable 的接口如下:

interface LoxCallable {
    int arity();
    Object call(Interpreter interpreter, List<Object> arguments);
}

0X01 实现一个内嵌函数

按照上面的步骤我们生成了调用函数的「抽象语法树」,以及执行这个「抽象语法树」。

由于我们现在没有实现如何定义一个函数,所以我们没有函数可调用。

但是我们可以实现一个简单的「内嵌函数」clock(),原理如下:

  • 实现一个全局环境 global
  • 在这个全局环境中定义一个 clock 的函数接口,实现

代码如下:

private Environment environment = globals;

    Interpreter() {
        globals.define("clock", new LoxCallable() {
            @Override
            public int arity() {
                return 0;
            }

            @Override
            public Object call(Interpreter interpreter, List<Object> arguments) {
                return (double) System.currentTimeMillis() / 1000.0;
            }

            @Override
            public String toString() {
                return "<native fn>";
            }
        });
    }

这样就能使用 clock 函数了。

var a = clock();
print a;

0X02 函数的声明

现在我们写一下函数声明的「文法」:

declaration → funDecl
            | varDecl
            | statement ;
funDecl  → "fun" function ;
function → IDENTIFIER "(" parameters? ")" block ;

接着我们写出「函数声明」的「抽象语法树」的格式:

static class Function extends Stmt {
    Function(Token name, List<Token> params, List<Stmt> body) {
        this.name = name;
        this.params = params;
        this.body = body;
    }

    <R> R accept(Visitor<R> visitor) {
        return visitor.visitFunctionStmt(this);
    }

    final Token name;
    final List<Token> params;
    final List<Stmt> body;
}

然后在 parser 中,实现生成「抽象语法树」的代码:

    private Stmt declaration() {
        // 在这里抓错误
        try {
            if (match(VAR)) {
                return varDeclaration();
            }
            if (match(FUN)) {
                return function("function");
            }
            return statement();
        } catch (ParseError e) {
            // 进入 panic mode
            synchronize();
            return null;
        }
    }

    private Stmt.Function function(String kind) {
        Token name = consume(IDENTIFIER, "Expect " + kind + " name.");
        consume(LEFT_PAREN, "Expect '(' after " + kind + " name.");
        List<Token> parameters = new ArrayList<>();
        if (!check(RIGHT_PAREN)) {
            do {
                if (parameters.size() >= 255) {
                    error(peek(), "Cannot have more than 255 parameters.");
                }

                parameters.add(consume(IDENTIFIER, "Expect parameter name."));
            } while (match(COMMA));
        }
        consume(RIGHT_PAREN, "Expect ')' after parameters.");

        consume(LEFT_BRACE, "Expect '{' before " + kind + " body.");
        List<Stmt> body = block();
        return new Stmt.Function(name, parameters, body);
    }

最后执行「抽象语法树」(定义函数):

@Override
public Void visitFunctionStmt(Stmt.Function stmt) {
    LoxFunction function = new LoxFunction(stmt);
    environment.define(stmt.name.lexeme, function);
    return null;
}

这里的 LoxFunction 如下:

class LoxFunction implements LoxCallable {
    private final Stmt.Function declaration;

    LoxFunction(Stmt.Function declaration) {
        this.declaration = declaration;
    }

    @Override
    public int arity() {
        return declaration.params.size();
    }

    @Override
    public String toString() {
        return "<fn " + declaration.name.lexeme + ">";
    }

    @Override
    public Object call(Interpreter interpreter, List<Object> arguments) {
        Environment environment = new Environment(interpreter.globals);
        for (int i = 0; i < declaration.params.size(); i++) {
            environment.define(declaration.params.get(i).lexeme, arguments.get(i));
        }

        interpreter.executeBlock(declaration.body, environment);
        return null;
    }
}

而当我们执行函数的时候,会执行 function 的 call 函数,我们来看看 call 函数做了什么:

// 创建了一个新的环境,并把全局环境给了他
// 在新的环境中定义传过来的参数
public Object call(Interpreter interpreter, List<Object> arguments) {
    Environment environment = new Environment(interpreter.globals);
    for (int i = 0; i < declaration.params.size(); i++) {
        environment.define(declaration.params.get(i).lexeme, arguments.get(i));
    }

    interpreter.executeBlock(declaration.body, environment);
    return null;
}

这样我们就实现了一个可定义,可调用的函数。最后我们再来看看如何返回值

0X03 函数的返回

首先,我们写出 return 的文法:

statement  → exprStmt
           | forStmt
           | ifStmt
           | printStmt
           | returnStmt
           | whileStmt
           | block ;

returnStmt → "return" expression? ";" ;

在我们实现的语言中,不管有没有写 return 函数,其实都有返回值等价于:

return nil;

写出 return 的「抽象语法树」的格式:

    static class Return extends Stmt {
        Return(Token keyword, Expr value) {
            this.keyword = keyword;
            this.value = value;
        }

        <R> R accept(Visitor<R> visitor) {
            return visitor.visitReturnStmt(this);
        }

        final Token keyword;
        final Expr value;
    }

实现生成抽象语法树的代码:


private Stmt returnStatement() {
    Token keyword = previous();
    Expr value = null;
    if (!check(SEMICOLON)) {
        value = expression();
    }

    consume(SEMICOLON, "Expect ';' after return value.");
    return new Stmt.Return(keyword, value);
}

if (match(RETURN)) {
    return returnStatement();
}

执行抽象语法树:

@Override
public Void visitReturnStmt(Stmt.Return stmt) {
    Object value = null;
    if (stmt.value != null)
        value = evaluate(stmt.value);

    throw new Return(value);
}

并在 call 中抓住这个 Return exception:

try {
    interpreter.executeBlock(declaration.body, environment);
} catch (Return returnValue) {
    return returnValue.value;
}

这样就实现了 return 的语法

0X04 局部函数的实现

对于函数我们还有一个点没有实现:

fun makeCounter() {
  var i = 0;
  fun count() {
    i = i + 1;
    print i;
  }

  return count;
}

var counter = makeCounter();
counter(); // "1".
counter(); // "2".

这个叫做「闭包」

原理如下:

  • 当执行一个函数的时候,会产生一个属于这个函数的新环境,比如调用 makeCounter() 会产生一个新的环境,这个新的环境,会保存定义的所有变量

  • 当在外面执行内部函数的时候,由于内部函数保存了,定义它时的环境,所以就能够,访问之前环境定义的变量

代码如下:

@Override
public Void visitFunctionStmt(Stmt.Function stmt) {
    // environment 是当前环境
    LoxFunction function = new LoxFunction(stmt, environment);
    environment.define(stmt.name.lexeme, function);
    return null;
}
class LoxFunction implements LoxCallable {
    private final Stmt.Function declaration;
    private final Environment closure;

    LoxFunction(Stmt.Function declaration, Environment closure) {
        this.declaration = declaration;
        this.closure = closure;
    }

    @Override
    public int arity() {
        return declaration.params.size();
    }

    @Override
    public String toString() {
        return "<fn " + declaration.name.lexeme + ">";
    }

    @Override
    public Object call(Interpreter interpreter, List<Object> arguments) {
        Environment environment = new Environment(closure);
        for (int i = 0; i < declaration.params.size(); i++) {
            environment.define(declaration.params.get(i).lexeme, arguments.get(i));
        }

        try {
            interpreter.executeBlock(declaration.body, environment);
        } catch (Return returnValue) {
            return returnValue.value;
        }
        return null;
    }
}

至此我们「函数」相关知识的与代码已完成!

相关文章

  • 编译原理——函数!

    前言:函数的实现又是一个难点,我们一点一点攻破! 0X00 函数调用 按照惯例我们写出函数调用的「文法」: 这样的...

  • 2021.4.15 前端面试复习

    首屏加载慢的原理 webpack的loader编译原理 render函数原理 实现高阶组件对饿了么ui组件进行二次...

  • Kotlin内联函数

    kotlin内联函数是什么? Kotlin里使用关键字 inline 来表示内联函数。其原理就是:在编译时期,把调...

  • [学习vue3]编译原理[三]

    编译器原理 我们运行app之前执行编译操作compile => render()得到的渲染函数会在两种情况下h执行...

  • C++多态

    多态原理 当类存在虚函数时,编译器会为该类维护一个表,这个表就是虚函数表(vtbl),里面存放了该类虚函数的函数指...

  • C++多态——虚函数表vtable

    纯Swift类的函数调用原理,类似于C++的虚函数表 纯Swift类的函数调用,类似于C++的虚函数表,是编译时决...

  • 十七、多态(二)

    1. 多态的实现原理 1.1虚函数表和vptr指针 当类中声明虚函数时,编译器会在类中生成一个虚函数表; 虚函数表...

  • static

    这里给大家再介绍一个防御技巧-利用static关键字裁掉函数符号。 原理 如果函数属性为static,那么编译时该...

  • 编译原理

    编译原理 标签(空格分隔): 编译原理 编译和解释 编译 整个程序全部翻译结束之后,程序才能开始运行;编译和运行是...

  • C++ 函数重载

    函数重载 特性 和参数类型有关 参数个数有关 函数名相同 和返回值无关 原理 C++编译器(MSVC,G++,GC...

网友评论

    本文标题:编译原理——函数!

    本文链接:https://www.haomeiwen.com/subject/tillvctx.html