用java寫一個lisp 直譯器

yangrd發表於2022-02-07

起初最早聽到lisp這個名字是一個偶然的機會,留下了很牛的印象,時間匆匆五年就過去了,前些日子看sicp,裡面又再次提到了這個名字,從網上找了幾個入門文件學習了一下基礎語法,便又繼續看起了sicp;從寫下第一行(+ 1 2)程式碼,日子轉眼一個月就過去了,不得不說 lisp的字首表示式的方式還是很不錯的,不知怎得慢慢有了寫一個lisp 直譯器的想法,然後想到了之前王垠似乎寫過一篇文章《怎樣寫一個直譯器》 如果你對如何寫一個直譯器感興趣可以看下這篇文章,還是有所啟發的。

回來了嗎?哈哈哈

我們繼續旅途,這裡選用的lisp方言是Scheme,選用它並無其他,只是因為當初學習的基礎語法是它

寫一個lisp直譯器可以分三步:

1.將lisp表示式的字串轉換成一個樹性結構的陣列
2.解釋這棵樹
3.支援變數和方法呼叫

這裡使用的語言是java

構造語法樹

首先第一步:如何將lisp表示式的字串轉換成一個樹性結構的陣列?
我們看一幾個lisp表示式 分析一下他的構成

(+ 1 2)
(+ 1 2 (- 3 4))
(+ 1 2 (- 3 4) 5)
(+ 1 2 (- 3 4) 5 (+ 6 7 (+ 8 9)))
(+ 1 2 (- 3 4) (+ 5 6 (+ 7 8)))
(+ 1 2 (- 3 4) (+ 5 6 (+ 7 8)) 9)

可以看到以上表示式裡面可以分成兩種元素:一個是不可分割的最小元素如 + - 1 2 3 這種 ,還有 (- 3 4)這種複合元素,而複合元素也是由最小的基礎元素構成,於是我們得到了第一個規則(複合元素可以被拆分成更小的基礎元素和複合元素)
以(+ 1 2 (- 3 4) 5 (+ 6 7 (+ 8 9)))這個表示式為例,它如果是一棵樹長什麼樣子呢?下面讓我們畫出它的形態:
(+ 1 2 (- 3 4) 5 (+ 6 7 (+ 8 9)))
我們有了它的樣子,但要如何將一個字串形式的表示式轉換成這樣一棵樹呢? 這是我們接下來要分析的問題
duang duang duang duang...
讓我們回到我們的第一個規則 這裡還有什麼隱藏資訊呢?
1.複合元素可拆分
2.基礎元素不可拆分
3.複合元素是被“()”包裹的元素
有了這三項我們就可以在進一步的思考了,樹樹樹,樹的元素是什麼?
1.節點
2.葉子節點
眉目,眉目,有了眉目
節點對應的是複合元素,基礎元素對應的是葉子節點,那如何區分複合元素和基礎元素呢?
“3.複合元素是被“()”包裹的元素”,是它,是它,就是它。
複合節點裡的第一個元素是“(”後的第一個元素,最後一個元素是“)”前的第一個元素,我們又得到了第二個規則;有了上面兩個規則我們開始構建我們的第一棵樹:
程式碼:

node

class ExpNode implements Iterable<Object>{

   List<Object> data = new ArrayList<>();
   Node parent;

   public static ExpNode newInstance() {
        return new ExpNode();
   }

    public void add(Object o) {
        if (o instanceof ExpNode) {
            ((ExpNode) o).parent = this;
        }
        super.add(o);
    }

    public Iterator<Object> iterator() {
        return data.iterator();
    }
}

parse

public class Parse {

    public static ExpNode parseTree(String exp) {
        ExpNode root = ExpNode.newInstance();
        buildNode(trimStr(exp), 0, root);
        return (ExpNode) root.iterator().next();
    }

    private static void buildNode(String exp, int level, Cons node) {
        int lIndex = exp.indexOf("(");
        int rIndex = exp.indexOf(")");
        if (rIndex > -1) {
            if (lIndex < rIndex && lIndex > -1) {
                String subExp = exp.substring(lIndex, rIndex + 1);
                if (isBaseLeaf(exp)) {
                    ExpNode a = parseNode(subExp).orElseThrow(RuntimeException::new);
                    node.add(a);
                } else {
                    Optional<ExpNode> nodeOptional = parseNode(subExp);
                    if (nodeOptional.isPresent()) {
                        ExpNode val = nodeOptional.get();
                        node.add(val);
                        node = val;
                    } else {
                        ExpNode objects = ExpNode.newInstance();
                        node.add(objects);
                        node = objects;
                    }
                }
                ++level;
                log.debug("{}{}---{}", createRepeatedStr(level), exp.substring(lIndex), subExp);
                buildNode(exp.substring(lIndex + 1), level, node);
            } else {
                //) b a (+ 8 9) => ) b a ( => b a
                if (lIndex > -1) {
                    String subExp = trimStr(exp.substring(rIndex + 1, lIndex));
                    if (subExp.length() > 0 && !subExp.contains(")")) {
                        String[] values = subExp.split(" ");
                        for (String val : values) {
                            node.parent().add(parseObj(val));
                        }
                    }
                } else {
                    // 所有都是後退
//                    ) b a) => b a) => b a
                    String subExp = exp.substring(rIndex + 1);
                    int r2Index = 1 + subExp.indexOf(")");
                    if (r2Index > 1) {
                        subExp = trimStr(subExp.substring(1, r2Index - 1));
                        if (subExp.length() > 0) {
                            String[] values = subExp.split(" ");
                            for (String val : values) {
                                node.parent().add(parseObj(val));
                            }
                        }
                    }
                }
                --level;
                log.debug("{}{}", createRepeatedStr(level), exp.substring(rIndex));
                buildNode(exp.substring(rIndex + 1), level, node.parent());
            }
        } else {
            log.debug(createRepeatedStr(level));
        }

    }

    private static Optional<ExpNode> parseNode(String exp) {
        String subExp = "";
        if (isBaseLeaf(exp)) {
            //(xx [xx])
            subExp = exp.substring(1, exp.length() - 1);
        } else {
            // (xx [xx] (xx xx xx)
            // (xx [xx] (
            // ((
            subExp = exp.substring(1);
            subExp = subExp.substring(0, subExp.indexOf("("));
            if (subExp.trim().isEmpty()) {
                return Optional.empty();
            }
        }
        String[] keys = subExp.split(" ");
        ExpNode node = ExpNode.newInstance();
        for (int i = 0; i < keys.length; i++) {
            node.add(parseObj(keys[i]));
        }
        return Optional.of(node);
    }

    private static Object parseObj(String val) {
        try {
            return Integer.valueOf(val);
        } catch (NumberFormatException e) {
            if (val.equals("true") || val.equals("false")) {
                return Boolean.valueOf(val);
            } else if (val.indexOf("'") == 0 && val.lastIndexOf("'") == val.length() - 1) {
                return val.replaceAll("\"", "\"");
            } else {
                return val;
            }
        }
    }

    private static boolean isBaseLeaf(String exp) {
        return count(exp, "\\(") == 1 && count(exp, "\\)") == 1 && exp.matches("^\\(.+?\\)$");
    }

    private static int count(String str, String regx) {
        Matcher matcher = Pattern.compile(regx).matcher(str);
        int i = 0;
        while (matcher.find()) {
            i++;
        }
        return i;
    }

    private static String trimStr(String str) {
        String tempStr = str.replace("  ", " ").trim();
        return tempStr.contains("  ") ? trimStr(tempStr) : tempStr;
    }

    private static String createRepeatedStr(int n) {
        return String.join("", Collections.nCopies(n, "--"));
    }
}

當然還有更簡單的方式 哈哈哈,將“(”替換成“[”,將“)”轉換成“]”,讓後用json 解析器解析即可,哈哈哈哈

直譯器

在lisp 中語法構成的是列表+字首表示式的方式來表達,列表可以對應我們剛才提到的複合元素;關於字首表示式,這裡以我們熟悉的中綴表示式為例:1+2+5 這種,其中的 1,2,5 是運算數,+是運算子,運算子在其運算數中間的方式,字首表示式則是運算子在其運算數最前面的方式,當然還有字尾表示式。
我們還是以 (+ 1 2 (- 3 4) 5 (+ 6 7 (+ 8 9))) 為例:
我們將其放入進我們的Parse.parseTree()中會得到一個形狀是

[
    + 
    1 
    2
    [
        - 
        3
        4
    ]
    5
    [
        +
        6
        7
        [
            +
            8
            9
        ]    
    ]
]

這樣的一個巢狀列表,然後我們對其進行解釋
結合剛才字首表示式的構成我們會發現列表中的第一個元素是運算子,後面的可以是運算數或列表,然後我們對ExpNode 部署兩個方法: 一個是 car方法 用於獲取列表內第一個元素,一個是 cdr方法 用於獲取列表內去掉第一個元素的剩餘元素:

class ExpNode implements Iterable<Object>{

    List<Object> data = new ArrayList<>();
    ...

    public Object car(){
        return data.get(0);
    }

    public ExpNode cdr(){
         List<Object> subData = getData().subList(1, this.getData().size());
        return ExpNode.one(this, subData.toArray()); 
    }

    private static ExpNode one(ExpNode exp, Object... vs) {
        ExpNode objects = ExpNode.newInstance();
        for (Object o : vs) {
            objects.add(o);
        }
        objects.parent = exp;
        return objects;
    }

    ...
}

開始構造我們第一個直譯器:

public class JLispInter {

     public static Object inter(ExpNode exp){
        Object car = exp.car();
        ExpNode cdr = exp.cdr();
        if (car instanceof String){
            switch ((String)car){
                case "+":
                    return cdr.data().stream().map(JLispInter::getAtom).mapToInt(o->(Integer)o).sum();
                case "-":
                    return cdr.data().stream().map(JLispInter::getAtom).mapToInt(o->(Integer)o).reduce((x,y)->x-y).orElseThrow(RuntimeException::new);
                case "*":
                    return cdr.data().stream().map(JLispInter::getAtom).mapToInt(o->(Integer)o).reduce((x,y)->x*y).orElseThrow(RuntimeException::new);
                case "/":
                    return cdr.data().stream().map(JLispInter::getAtom).mapToInt(o->(Integer)o).reduce((x,y)->x/y).orElseThrow(RuntimeException::new);
                default:
                    return car;
            }
        }else if (car instanceof ExpNode){
            Object o = inter((ExpNode) car);
            return cdr.isEmpty()?o:inter(cdr);
        }else {
            return cdr.isEmpty()?car:inter(cdr);
        }
    }

    private static Object getAtom(Object o){
        if (o instanceof ExpNode){
            return inter((ExpNode) o);
        }else{
            return o;
        }
    }
}

發現ExpNode還需要部署(data 及 isEmpty) 方法

class ExpNode implements Iterable<Object>{

    List<Object> data = new ArrayList<>();
    ...

    public List<Object> data(){return data};
    public boolean isEmpty(){return data.isEmpty()}
    ...
}

輸入

    public static void main(String[] args) {
        System.out.println(inter(Parse.parseTree("(+ 1 2 (- 3 4) 5 (+ 6 7 (+ 8 9)))")));
    }

會得到

37

雖然這個簡單直譯器只支援四則運算,但它已經具備瞭解析巢狀列表的能力,為我們後續(如作用域)提供了堅實的基礎,後面我們會在這個直譯器的基礎上增加對 define lambda 關鍵字的支援,而這些都要依賴兩個東西一個是作用域另一個是變數,這兩個都是重大的特性,尤其是變數,稍作休息,喝杯水,繼續我們的旅途。

支援變數和方法呼叫

首先讓我們在上一個直譯器的基礎上實現變數的功能,這很簡單,關鍵是把變數和對應的值進行繫結,在java 裡面我們可以通過Map來實現它。
定義一個Map我們給他起個名字叫env

Map<String,Object> env = new HashMap<>();

在env 的基礎上我們再定義兩個方法 分別是定義一個值define(k,v),env(k)

    public static void define(String k, Object v){
        env.put(k,v)
    }

    public static Optional<Object> env(String key){
        return Optional.of(env.get(key));
    }

對 JLispInter 新增一些功能

public class JLispInter {

     public static Object inter(ExpNode exp){
        Object car = exp.car();
        ExpNode cdr = exp.cdr();
        if (car instanceof String){
            switch ((String)car){
                ...
                   case "define":
                    define((String)cdr.car(), inter(cdr.cdr()));
                    return null;
                default:
                   return env((String)car).orElse(car);
            }
        }else if (car instanceof ExpNode){
            Object o = inter((ExpNode) car);
            return cdr.isEmpty()?o:inter(cdr);
        }else {
            return cdr.isEmpty()?car:inter(cdr);
        }
    }

    private static Object getAtom(Object o){
         if (o instanceof ExpNode){
            return inter((ExpNode) o);
        }else if (o instanceof String){
            return env((String)o).orElse(null);
        }else {
            return o;
        }
    }
}

輸入

 System.out.println(inter(Parse.parseTree("((define a 5) (+ a 6))")));
 System.out.println(inter("((define a 5) (define b 8) (+ a b))"));

輸出

11
13

現在我們已經支援

  • 變數:a
  • 繫結:(define a 5)
  • 四則運算: + - * /

接下來我們再支援 方法 及 呼叫
方法此處我們選擇支援 lambda ,lambda表示式形式如下:
(lambda (x) e)
開始前 我們先對解析器做些改造增加一個符號型別,為了方便後續與String 區分開
Parse

public class Parse {
...
   private static Object parseObj(String val) {
        try {
            return Integer.valueOf(val);
        } catch (NumberFormatException e) {
            if (val.equals("true") || val.equals("false")) {
                return Boolean.valueOf(val);
            } else if (val.indexOf("'") == 0 && val.lastIndexOf("'") == val.length() - 1) {
                return val.replaceAll("\"", "\"");
            } else {
                return Symbols.of(val);
            }
        }
    }
...
}

Symbols

public interface Symbols {
    static Symbols of(String name) {
        return new Symbols.SimpleVar(name);
    }

    String getName();

    @Value
    class SimpleVar implements Symbols {
        String name;

        @Override
        public String toString() {
            return "`" + name;
        }
    }
}

接著我們再對環境進行改造讓其可以將 lambda 中的變數與 define 中定義的變數區分開來,並支援當前作用域不存在的變數向上查詢,都沒有找到報異常。
Env

    public static class Env{

        private  final Map<String,Object> env = new HashMap<>();
        private Env parent;

        public static Env newInstance(Env parent){
            Env env1 = new Env();
            env1.parent = parent;
            return env1;
        }
        
        public  void define(String key,Object val){
            env.put(key,val);
        }

        public  Optional<Object> env(Symbols symbols){
            String symbolsName = symbols.getName();
            return Optional.ofNullable(env.containsKey(symbolsName)?env.get(symbolsName):(parent!=null?parent.env(symbols).orElse(null):null));
        }
    }

JLispInter

public class JLispInter {
   public static Object inter(String exp){
      return inter(Parse.parseTree(exp),Env.newInstance(null));
    }

    // 1
    public static Object inter(ExpNode exp,Env env){
        Object car = exp.car();
        ExpNode cdr = exp.cdr();
        if (car instanceof Symbols){
            switch (((Symbols) car).getName()){
                case "+":
                    //2
                    return cdr.data().stream().map(o->getAtom(o,env)).mapToInt(o->(Integer)o).sum();
                case "-":
                    return cdr.data().stream().map(o->getAtom(o,env)).mapToInt(o->(Integer)o).reduce((x,y)->x-y).orElseThrow(RuntimeException::new);
                case "*":
                    return cdr.data().stream().map(o->getAtom(o,env)).mapToInt(o->(Integer)o).reduce((x,y)->x*y).orElseThrow(RuntimeException::new);
                case "/":
                    return cdr.data().stream().map(o->getAtom(o,env)).mapToInt(o->(Integer)o).reduce((x,y)->x/y).orElseThrow(RuntimeException::new);
                case "define":
                    env.define(cdr.carSymbols().getName(), inter(cdr.cdr(),env));
                    return null;
                case "lambda":
                    return (Function<Object[],Object>) (x)->{
                        ExpNode args = (ExpNode)cdr.car();
                        ExpNode body = cdr.cdr();
                        validateTrue(args.data().size()==x.length,"引數不一致");
                        //3
                        Env env0 = Env.newInstance(env);
                        int i = 0;
                        for (Object argName : args) {
                            env0.define(((Symbols)argName).getName(), x[i]);
                            i++;
                        }
                        return inter(body,env0);
                    };
                default:
                    Optional<Object> env1 = env.env((Symbols)car);
                    // 4
                    boolean isApply = env1.isPresent()&&env1.get() instanceof Function&&exp.isExp();
                    if(isApply){
                        Object v = env1.get();
                        Function<Object[],Object> f = (Function<Object[], Object>) v;
                       return f.apply(cdr.data().stream().map(o -> getAtom(o, env)).toArray());
                    }
                    return env1.orElse(car);
            }
        }else if (car instanceof ExpNode){
            Object o = inter((ExpNode) car,env);
            return cdr.isEmpty()?o:inter(cdr,env);
        }else {
            return cdr.isEmpty()?car:inter(cdr,env);
        }
    }

    private static Object getAtom(Object o,Env env){
        if (o instanceof Cons){
            return inter((Cons) o,env);
        }else if (o instanceof Symbols){
            //5
            return env.env((Symbols) o).orElseThrow(()->new IllegalArgumentException(o+"不存在"));
        }else {
            return o;
        }
    }

    private static void validateTrue(boolean flag, String err){
        if (!flag){
            throw new IllegalArgumentException(err);
        }
    }
}

當我們輸入

 System.out.println(inter("((define f (lambda (x y) (+ x y))) (f 9 8))"));
System.out.println(inter("((define add (lambda (x y) (+ x y)))(define f (lambda (o x y) (o x y))) (f add 9 8))"));

會輸出

17
17

標記1處我們增加了env 環境變數的入參,使一開始與全域性環境繫結 exp 可以與不同的環境進行繫結,從而與全域性環境解耦,為後續作用域提供支援。
標記2處將獲取資料的從全域性變數環境改為具體的上下文變數環境
標記3處每個lambda表示式有自己的變數環境,並且擁有外部的變數環境引用,方便後面可以支援變數向外部變數環境查詢
標記4處增加了判斷當前表示式第一個元素是否是變數並且繫結的值是否是函式,是的話再(通過isExp)判斷是否是表示式(完整表示式,想反的是通過 exp.cdr 擷取出來的 表示式片段),然後進行呼叫 (apply)
標記5處未繫結值的變數丟擲異常

總結

此時我們寫的JLispInter已經支援了

  • 變數:a
  • 繫結:(define a 5)
  • 四則運算: + - * /
  • 函式 (lambda (x) (exp))
  • 呼叫 (g exp)

當然還有許多不完善的地方,如解析器還不支援字串,高階函式中四則運算 define 等還不能作為入參,不支援匿名函式 ,分支判斷 等,這些後面如果有時間的話會寫一篇新的文章, 劇透一下支援這些功能的解析器在去年年底就寫出來了,如果只是貼程式碼的話,我想這篇文章會很快。

相關文章