技术预计阅读 13 分钟5036 字0 次浏览

Antlr4系列⑩:支持数组

array类型、字面量、下标读写、len

目录

前几篇文章连续实现了调试器的能力,我们的小型DSL解释器已经能够被断点、单步调试了。这篇文章回到语言本身,继续补完语言的基础能力——数组(严格来说是"元素类型统一为数值的动态数组",不支持不同类型混装,保持和函数参数一样的简化设计)。

一、语法设计

  1. 数组定义

    参考主流语言,用方括号包裹一组用逗号分隔的值作为数组字面量,比如[1, 2, 3]。定义一个数组类型的变量,复用已有的number/string/boolean风格,新增array关键字:array arr = [1, 2, 3]

  2. 读取元素 / 修改元素

    arr[下标]读取数组中某个位置的值,下标从0开始;arr[下标] = 值修改数组中某个位置的值。

  3. 数组长度

    新增内置的len(arr)获取数组长度,配合while循环即可遍历整个数组,不需要额外设计for-each这样的新语法。

二、语法定义

根据以上设计新增语法:

setArg :'number'  id = ID '='  calcu                      #numarg
       |'string'  id = ID '='  STRING                     #chararg
       |'boolean' id = ID '='  BOOL                       #boolarg
       |'array'   id = ID '='  arrayLiteral                #arrayarg  // 新增
       ;

// 新增:数组字面量,方括号包裹0个或多个用逗号分隔的calcu
arrayLiteral: '[' (calcu (',' calcu)*)? ']'
            ;

calcu: calcu opt=(MUL|DIV) calcu                          # mulAndDiv
     | calcu opt=(ADD|SUB) calcu                          # addAndSub
     | '(' calcu ')'                                      # parens
     | NUMBER                                             # number
     | ID '(' (calcu (',' calcu)*)? ')'                   # funcCall
     | LEN '(' ID ')'                                      # arrayLen  // 新增
     | ID '[' calcu ']'                                    # arrayGet  // 新增
     | ID                                                 # argValue
     ;

// assignStmt新增一个分支,用于修改数组中某个下标的值
assignStmt: id = ID '=' calcu                              #numAssign
          | id = ID '=' STRING                              #charAssign
          | id = ID '=' BOOL                                #boolAssign
          | arr = ID '[' index = calcu ']' '=' value = calcu #arrayElemAssign  // 新增
          ;

//以下为本篇新增词法
LEN : 'len' ;

arrayLiteral里的元素直接复用calcu,意味着数组的每个元素既可以是字面量,也可以是变量甚至一个完整的四则运算表达式,比如array arr = [a + 1, b * 2]arr[下标]这种写法在calcu里新增了一个arrayGet分支,在assignStmt里新增了一个arrayElemAssign分支,分别对应"读"和"写"两种场景——这和之前"定义用setArg,赋值用assignStmt"是同一种思路的延续。

三、语法实现

先扩展一下VisitorResult,之前只支持数值、字符串、布尔三种类型,现在加上数组(内部用List<Double>表示):

private VisitorResult(Object value) {
    if (value instanceof String
            || value instanceof Double
            || value instanceof Boolean
            || value instanceof List
            || value == null) {
        this.value = value;
    } else {
        throw new RuntimeException("不支持的参数类型" + value);
    }
}

@SuppressWarnings("unchecked")
public List<Double> getArray() {
    if (isArray()) {
        return (List<Double>) value;
    }
    throw new RuntimeException("非Array类型");
}

public boolean isArray() {
    return this.value instanceof List;
}

接着在MyRuleSetVisitor里补充数组的定义、字面量求值、读取、长度:

/**
 * 定义数组类型变量,先求出数组字面量对应的List,再保存到当前作用域
 */
@Override
public VisitorResult visitArrayarg(RuleSetParser.ArrayargContext ctx) {
    VisitorResult array = visit(ctx.arrayLiteral());
    currentScope.define(ctx.id.getText(), array.getArray());
    return VisitorResult.nil();
}

/**
 * 数组字面量,依次对每个元素求值后收集成一个List
 */
@Override
public VisitorResult visitArrayLiteral(RuleSetParser.ArrayLiteralContext ctx) {
    List<Double> values = new ArrayList<>();
    for (RuleSetParser.CalcuContext element : ctx.calcu()) {
        values.add(visit(element).getNumber());
    }
    return VisitorResult.value(values);
}

/**
 * 按下标读取数组元素
 */
@Override
public VisitorResult visitArrayGet(RuleSetParser.ArrayGetContext ctx) {
    String name = ctx.ID().getText();
    List<Double> array = getArray(name);
    int index = visit(ctx.calcu()).getNumber().intValue();
    checkIndex(name, array, index);
    return VisitorResult.value(array.get(index));
}

/**
 * len(数组),返回数组长度
 */
@Override
public VisitorResult visitArrayLen(RuleSetParser.ArrayLenContext ctx) {
    List<Double> array = getArray(ctx.ID().getText());
    return VisitorResult.value((double) array.size());
}

private List<Double> getArray(String name) {
    Object value = currentScope.get(name);
    if (!(value instanceof List)) {
        throw new RuntimeException("未定义的数组:" + name);
    }
    return (List<Double>) value;
}

private void checkIndex(String name, List<Double> array, int index) {
    if (index < 0 || index >= array.size()) {
        throw new RuntimeException("数组" + name + "下标越界:" + index + ",数组长度为" + array.size());
    }
}

修改元素值的实现有一个值得说的细节:

/**
 * 修改数组中某一个下标的值。数组本身是可变的List,被currentScope持有的是同一个引用,
 * 这里直接对取出来的List调用set,不需要像标量变量那样再调用一次assign把新值写回作用域,
 * 因为"修改数组里某个元素"和"数组变量本身指向另一个对象"是两回事。
 */
@Override
public VisitorResult visitArrayElemAssign(RuleSetParser.ArrayElemAssignContext ctx) {
    List<Double> array = getArray(ctx.arr.getText());
    int index = visit(ctx.index).getNumber().intValue();
    checkIndex(ctx.arr.getText(), array, index);
    array.set(index, visit(ctx.value).getNumber());
    return VisitorResult.nil();
}

关于引用语义:前几篇文章里,number/string/boolean变量的赋值都要调用Scope.assign重新把新值put进作用域,是因为这些值本身是不可变的(Java的DoubleStringBoolean都是不可变对象),"改值"唯一的办法就是"换一个新对象塞回去"。而数组用的是Java的ArrayList,本身是可变对象,Scope里保存的其实是这个List对象的引用。所以arr[1] = 99不需要调用assign把一个新的List重新塞回作用域,直接对拿到的这个List调用set修改其中一项即可——这也意味着,如果把同一个数组传给两处(目前的语言还没有支持把数组当函数参数传递,但设计上要提前想到这一点),两边修改的会是同一份数据,这是可变对象天然带来的"共享"特性,后续如果要支持数组作为函数参数,需要格外注意这一点。

另外值得一提的是,print(arr)不需要任何额外改造就能直接支持打印数组——visitPrintArg本来就是拿到变量的原始值直接调用System.out.println,Java的List本身有现成的toString()实现(会打印成[10.0, 20.0, 30.0]这样的格式),正好够用。

四、测试代码与执行结果

String expression =
        "array arr = [10, 20, 30] \n" +
                "print(arr) \n" +
                "number first = arr[0] \n" +
                "print(first) \n" +
                "arr[1] = 99 \n" +
                "print(arr) \n" +
                "number i = 0 \n" +
                "number sum = 0 \n" +
                "while (i < len(arr)) { \n" +
                "    sum = sum + arr[i] \n" +
                "    i = i + 1 \n" +
                "} \n" +
                "print(sum) \n" +
                "number bad = arr[10]";     // 故意制造一个越界访问
calcute(expression);

执行结果如下:

执行:
array arr = [10, 20, 30]
print(arr)
number first = arr[0]
print(first)
arr[1] = 99
print(arr)
number i = 0
number sum = 0
while (i < len(arr)) {
    sum = sum + arr[i]
    i = i + 1
}
print(sum)
number bad = arr[10]

[10.0, 20.0, 30.0]
10.0
[10.0, 99.0, 30.0]
139.0
Exception in thread "main" java.lang.RuntimeException: 数组arr下标越界:10,数组长度为3
	at cn.irule.MyRuleSetVisitor.checkIndex(MyRuleSetVisitor.java:276)
	......

依次验证:数组字面量定义并打印[10.0, 20.0, 30.0];按下标读取arr[0]得到10.0arr[1] = 99修改元素后再打印,变成[10.0, 99.0, 30.0],说明修改是原地生效的;用while配合len(arr)遍历数组求和,10 + 99 + 30 = 139.0,结果正确;最后故意访问下标10(数组只有3个元素),程序抛出了预期的越界异常,符合边界检查的设计。

五、遗留的问题

目前数组的元素只能是数值,还不支持字符串数组或者混合类型数组,也还没有支持把数组当作参数传给函数(第⑦篇实现的函数目前只接受数值参数)。这些都是可以在数组这个基础上继续扩展的方向。下一篇文章我们计划把目光转向字符串相关的能力:字符串拼接、和数值之间的相互转换,以及内置函数的注册机制,让len(arr)这样的写法未来能够以一种更通用、可扩展的方式支持更多内置能力。

花开空白

西安

相关文章

评论(0)

还没有评论,来抢沙发吧

发表评论