[JAVA] Combinateur bibliothèque-analyseur Kinx (partie 2)

Combinateur bibliothèque-analyseur Kinx (partie 2)

introduction

** "Ressemble à JavaScript, le cerveau (contenu) est Ruby, (la stabilité est AC / DC)" ** Langage de script Kinx ). Cette fois, c'est aussi un combinateur d'analyseur.

La dernière fois, je suis allé au point de créer un AST dans Kinx Library-Parser Combinator (Part 1).

Cette fois, je vais l'interpréter et l'exécuter. Enfin, JIT compile et s'exécute.

avant ça

la revue

Reconfirmons le dernier programme final.

using Parsek;

function makeAST(first, rest) {
    var expr = first;
    for (var i = 0, l = rest.length(); i < l; ++i) {
        expr = { lhs: expr, op: rest[i][0], rhs: rest[i][1] };
    }
    return expr;
}

var $ = new Parsek();
var number = $.regex(/[1-9][0-9]*|[0-9]/).map(Integer.parseInt);
var addsub = $.oneOf("+-");
var muldiv = $.oneOf("*/%");
var lbr = $.string("(");
var rbr = $.string(")");

var expression;
var factor = $.lazy(&() => $.alt(number, $.seq(lbr, expression, rbr).map(&(value) => value[1])));
var term = $.seqMap(factor, $.seq(muldiv, factor).many(), makeAST);
expression = $.seqMap(term, $.seq(addsub, term).many(), makeAST);

// test
System.println(expression.parseAll("1+2*3+2*(14-2)").value.toJsonString(true));

Cette. Résolvons un seul problème avec cela. Ce n'est pas un problème, cependant.

En fait, cet analyseur échouera s'il y a un caractère vide. Bien sûr, il n'y a pas de place pour interpréter les blancs, donc les caractères ne sont pas censés rencontrer les blancs, donc ils répondent avec confiance "Il n'y a pas de grammaire!".

Processus de saut de caractère vide

Dans les langages de programmation ordinaires, les sauts de jeton sont n'importe quel nombre de caractères vides, et il est courant que les blancs soient ignorés. Par conséquent, sautons les caractères vides. Il y a deux choses à utiliser:

En utilisant ces deux, vous pouvez utiliser .skip ($ .optWhitespace) pour ignorer les caractères vides après un parseur particulier correspondant.

Ici, préparez une fonction appelée lexeme () comme indiqué ci-dessous, et ajoutez un processus pour sauter les caractères vides. ʻIgnore` doit être ajouté ici quand il y a plus de choses à ignorer, et il est nécessaire de sauter les caractères vides même au premier démarrage, alors définissez-le.

var ignore = $.optWhitespace;
var lexeme = &(p) => p.skip(ignore);

Cette fonction lexeme () doit seulement être à la rupture du jeton, donc elle devrait être dans l'analyseur de la plus petite unité de number, ʻaddsub, muldiv, lbr, rbr`. Si vous réécrivez le premier programme, ce sera comme suit.

using Parsek;

function makeAST(first, rest) {
    var expr = first;
    for (var i = 0, l = rest.length(); i < l; ++i) {
        expr = { lhs: expr, op: rest[i][0], rhs: rest[i][1] };
    }
    return expr;
}

var $ = new Parsek();
var ignore = $.optWhitespace;
var lexeme = &(p) => p.skip(ignore);

var number = lexeme($.regex(/[1-9][0-9]*|[0-9]/)).map(Integer.parseInt);
var addsub = lexeme($.oneOf("+-"));
var muldiv = lexeme($.oneOf("*/%"));
var lbr = lexeme($.string("("));
var rbr = lexeme($.string(")"));

var expression;
var factor = $.lazy(&() => $.alt(number, $.seq(lbr, expression, rbr).map(&(value) => value[1])));
var term = $.seqMap(factor, $.seq(muldiv, factor).many(), makeAST);
expression = $.seqMap(term, $.seq(addsub, term).many(), makeAST);

// test
System.println(expression.parseAll("1+2*3+2*(14-2)").value.toJsonString(true));
System.println(ignore.then(expression).parseAll("  1 + 2 * 3 + 2 * ( 14 - 2 )  ").value.toJsonString(true));
/* Both results are the same.
{
    "lhs": {
        "lhs": 1,
        "op": "+",
        "rhs": {
            "lhs": 2,
            "op": "*",
            "rhs": 3
        }
    },
    "op": "+",
    "rhs": {
        "lhs": 2,
        "op": "*",
        "rhs": {
            "lhs": 14,
            "op": "-",
            "rhs": 2
        }
    }
}
*/

Les deux renvoient le même résultat.

Le premier ʻignore.then () est la partie qui ignore le premier blanc. parser.then (anotherParser) renvoie ʻanotherParser, contrairement à .skip (). Cela entraînera l'ignorance de l'analyseur.

Entrons dans le sujet principal.

Interprète

Tout d'abord, implémentons l'interpréteur avant JIT. C'est plus simple. L'interpréteur s'exécute séquentiellement tout en parcourant l'AST. Évaluez le côté gauche, évaluez le côté droit et calculez avec le résultat. La classe ʻInterpreter` ressemble à ceci:

class Interpreter(opts_) {
    private sequence(r, op, lhs, rhs) {
        return if (!opts_.enableSequence);
        System.println("%d %s %d -> %d" % lhs % op % rhs % r);
    }
    public eval(ast) {
        var lhs = ast.lhs.isObject ? eval(ast.lhs) : ast.lhs;
        var rhs = ast.rhs.isObject ? eval(ast.rhs) : ast.rhs;
        var r = 0;
        switch (ast.op) {
        case '+':
            r = lhs + rhs;
            break;
        case '-':
            r = lhs - rhs;
            break;
        case '*':
            r = lhs * rhs;
            break;
        case '/':
            r = lhs / rhs;
            break;
        case '%':
            r = lhs % rhs;
            break;
        default:
            throw RuntimeException('Invalid operator');
        }
        sequence(r, ast.op, lhs, rhs);
        return r;
    }
}

J'ai aussi essayé de rendre le processus de calcul visible (passer {enableSequence: true} au constructeur montrera le processus de calcul). Lançons-le comme ça.

System.println(
    "Result = ",
    new Interpreter({ enableSequence: true })
        .eval(ignore.then(expression)
                    .parseAll("  1 + 2 * 3 + 2 * ( 14 - 2 )  ").value)
);
/*
2 * 3 -> 6
1 + 6 -> 7
14 - 2 -> 12
2 * 12 -> 24
7 + 24 -> 31
Result = 31
*/

Il est calculé en fonction de la priorité! Sans surprise, l'AST est construit selon la priorité. Essayons une autre formule.

System.println(
    "Result = ",
    new Interpreter({ enableSequence: true })
        .eval(ignore.then(expression)
                    .parseAll("(( 123 ) * 2 * 4 - 3 * ( 12 + 10 )) % 100").value)
);
/*
123 * 2 -> 246
246 * 4 -> 984
12 + 10 -> 22
3 * 22 -> 66
984 - 66 -> 918
918 % 100 -> 18
Result = 18
*/

Il y a.

Compilation JIT

C'est le JIT tant attendu. Cependant, bien que la technologie JIT elle-même soit merveilleuse, honnêtement, comprenez que l'interpréteur ci-dessus est plus rapide à ce niveau de calcul (pas de boucles ou de branches conditionnelles). Puisque je compile avec la même racine que l'interpréteur et que l'interpréteur lui-même ne fait pas grand-chose, le temps de compilation JIT est comme le temps de l'interpréteur (+ α). Hmmm.

C'est juste une base technologique. J'aurais dû faire un exemple qui fait une différence de vitesse. Souhaitez-vous réécrire Brainf * ck en tant que sujet, en vous référant à l'article ici?

Au fait, dans ʻInterpreter, la formule de calcul a été exécutée directement, mais ce n'est pas grave si vous la réécrivez afin que le code équivalent en langage machine soit sorti au lieu d'exécuter le calcul. En regardant la [Bibliothèque JIT](https://qiita.com/Kray-G/items/a237f195630f3f0a91df), la classe Compilerqui se traduit en code de langage machine ressemblera à ceci: C'est un peu long, mais je vais le dire tel quel. Encore une fois, vous pouvez générer un code de compilation au format intermédiaire en définissant{enableListing: true}` dans le constructeur.

class Compiler(opts_) {
    var regs_, regsLen_;
    enum { MOV, BOP, DIVMOD, RET }
    private initialize() {
        regs_ = [
            // Jit.R0 and Jit.R1 is used as a work register when it is division.
            { reg: Jit.R2, used: false, name: "R2" },
            { reg: Jit.R3, used: false, name: "R3" },
            { reg: Jit.R4, used: false, name: "R4" },
            { reg: Jit.R5, used: false, name: "R5" },
            { reg: Jit.S0, used: false, name: "S0" },
            { reg: Jit.S1, used: false, name: "S1" },
            { reg: Jit.S2, used: false, name: "S2" },
            { reg: Jit.S3, used: false, name: "S3" },
            { reg: Jit.S4, used: false, name: "S4" },
            { reg: Jit.S5, used: false, name: "S5" },
        ];
        regsLen_ = regs_.length();
    }
    private listing(type, op, dst, op1, op2) {
        return if (!opts_.enableListing);
        switch (type) {
        case MOV:
            System.println("%s <- %s" % dst % op1);
            break;
        case BOP:
            System.println("%s <- %s %s %s" % dst % op1 % op % op2);
            break;
        case DIVMOD:
            System.println("R0 <- %s" % op1);
            System.println("R1 <- %s" % op2);
            System.println("%s <- R0 %s R1" % dst % op);
            break;
        case RET:
            System.println("ret %s" % dst);
            break;
        }
    }
    private getReg() {
        for (var i = 0; i < regsLen_; ++i) {
            if (!regs_[i].used) {
                regs_[i].used = true;
                return i;
            }
        }
        throw RuntimeException("Not enough register");
    }
    private releaseReg(i) {
        regs_[i].used = false;
    }
    private compileLeaf(c, leaf) {
        var r = getReg();
        c.mov(regs_[r].reg, Jit.IMM(leaf));
        listing(MOV, null, regs_[r].name, leaf);
        return r;
    }
    private compileNode(c, ast) {
        var rl = ast.lhs.isObject ? compileNode(c, ast.lhs) : compileLeaf(c, ast.lhs);
        var rr = ast.rhs.isObject ? compileNode(c, ast.rhs) : compileLeaf(c, ast.rhs);
        var r = getReg();
        switch (ast.op) {
        case '+':
            c.add(regs_[r].reg, regs_[rl].reg, regs_[rr].reg);
            listing(BOP, ast.op, regs_[r].name, regs_[rl].name, regs_[rr].name);
            break;
        case '-':
            c.sub(regs_[r].reg, regs_[rl].reg, regs_[rr].reg);
            listing(BOP, ast.op, regs_[r].name, regs_[rl].name, regs_[rr].name);
            break;
        case '*':
            c.mul(regs_[r].reg, regs_[rl].reg, regs_[rr].reg);
            listing(BOP, ast.op, regs_[r].name, regs_[rl].name, regs_[rr].name);
            break;
        case '/':
            c.mov(Jit.R0, regs_[rl].reg);
            c.mov(Jit.R1, regs_[rr].reg);
            c.sdiv();
            c.mov(regs_[r].reg, Jit.R0);
            listing(DIVMOD, ast.op, regs_[r].name, regs_[rl].name, regs_[rr].name);
            break;
        case '%':
            c.mov(Jit.R0, regs_[rl].reg);
            c.mov(Jit.R1, regs_[rr].reg);
            c.sdivmod();
            c.mov(regs_[r].reg, Jit.R1);
            listing(DIVMOD, ast.op, regs_[r].name, regs_[rl].name, regs_[rr].name);
            break;
        default:
            throw RuntimeException('Invalid operator');
        }
        releaseReg(rl);
        releaseReg(rr);
        return r;
    }
    public compile(ast) {
        var c = new Jit.Compiler();
        c.enter();
        var r = compileNode(c, ast);
        c.ret(regs_[r].reg);
        listing(RET, null, regs_[r].name);
        return c.generate();
    }
    public run(ast) {
        var code = compile(ast);
        return code.run();
    }
}

Les registres peuvent être utilisés de «R2» à «R5» et «S0» à «S5». Enregistrez «R0» et «R1» car ils doivent être utilisés pour la division. La méthode d'allocation des registres est "Réserver lors de l'utilisation et renvoyer une fois terminé".

Notez que les registres de la série «R» peuvent être détruits après l'appel de la fonction, mais cette fois la fonction n'est pas appelée, elle peut donc être traitée de la même manière que la série «S». Vous obtiendrez une erreur si vous manquez de registres disponibles, mais la plupart des valeurs intermédiaires sont de courte durée et devraient convenir. Si la valeur à enregistrer augmente, émettez le code pour l'enregistrer dans la mémoire. De plus, la bibliothèque JIT de Kinx a une zone de variables locales sur la pile, il est donc plus facile de l'allouer lorsque vous êtes à court.

Considérant sérieusement l'allocation de registre, c'est assez profond, mais dans le cas de JIT, ** l'optimisation ne prend pas beaucoup de temps ** est la théorie, donc cela suffit. Parce que c'est une perte de temps d'optimisation. Dans le cas de la pré-compilation (AOT Compilation), vous n'avez pas à vous soucier du temps de compilation, et plus le temps d'exécution est rapide, meilleure est l'optimisation, mais dans JIT, le temps de compilation est également dans le temps d'exécution, donc ce n'est pas le cas.

Calculons-le maintenant. N'oubliez pas ʻutiliser Jit; `.

using Parsek;
using Jit;

/*réduction*/
System.println(
    "Result = ",
    new Compiler({ enableListing: true })
        .run(ignore.then(expression)
                   .parseAll("  1 + 2 * 3 + 2 * ( 14 - 2 )  ").value)
);
/*
R2 <- 1
R3 <- 2
R4 <- 3
R5 <- R3 * R4
R3 <- R2 + R5
R2 <- 2
R4 <- 14
R5 <- 2
S0 <- R4 - R5
R4 <- R2 * S0
R2 <- R3 + R4
ret R2
Result = 31
*/

Il y a. Essayons un autre exemple.

System.println(
    "Result = ",
    new Compiler({ enableListing: true })
        .run(ignore.then(expression)
                   .parseAll("(( 123 ) * 2 * 4 - 3 * ( 12 + 10 )) % 100").value)
);
/*
R2 <- 123
R3 <- 2
R4 <- R2 * R3
R2 <- 4
R3 <- R4 * R2
R2 <- 3
R4 <- 12
R5 <- 10
S0 <- R4 + R5
R4 <- R2 * S0
R2 <- R3 - R4
R3 <- 100
R0 <- R2
R1 <- R3
R4 <- R0 % R1
ret R4
Result = 18
*/

Il y a aussi ceci.

Vous pouvez également vider le code de langage de la machine, alors essayons-le.

var code = new Compiler()
       .compile(ignore.then(expression)
                      .parseAll("  1 + 2 * 3 + 2 * ( 14 - 2 )  ").value);
code.dump();
System.println(code.run());

Sortie sous Linux.

       0:   53                                          push rbx
       1:   41 57                                       push r15
       3:   41 56                                       push r14
       5:   48 8b df                                    mov rbx, rdi
       8:   4c 8b fe                                    mov r15, rsi
       b:   4c 8b f2                                    mov r14, rdx
       e:   48 83 ec 10                                 sub rsp, 0x10
      12:   48 c7 c7 01 00 00 00                        mov rdi, 0x1
      19:   48 c7 c1 02 00 00 00                        mov rcx, 0x2
      20:   49 c7 c0 03 00 00 00                        mov r8, 0x3
      27:   49 89 cb                                    mov r11, rcx
      2a:   4d 0f af d8                                 imul r11, r8
      2e:   4a 8d 0c 1f                                 lea rcx, [rdi+r11]
      32:   48 c7 c7 02 00 00 00                        mov rdi, 0x2
      39:   49 c7 c0 0e 00 00 00                        mov r8, 0xe
      40:   49 c7 c3 02 00 00 00                        mov r11, 0x2
      47:   4c 89 c3                                    mov rbx, r8
      4a:   49 2b db                                    sub rbx, r11
      4d:   49 89 f8                                    mov r8, rdi
      50:   4c 0f af c3                                 imul r8, rbx
      54:   4a 8d 3c 01                                 lea rdi, [rcx+r8]
      58:   48 89 f8                                    mov rax, rdi
      5b:   48 83 c4 10                                 add rsp, 0x10
      5f:   41 5e                                       pop r14
      61:   41 5f                                       pop r15
      63:   5b                                          pop rbx
      64:   c3                                          ret
31

Lisons-le grossièrement.

La valeur de retour de la fonction est déterminée comme étant le registre rax dans x64, donc cela retournera le résultat.

Puis dans un autre exemple.

var code = new Compiler()
       .compile(ignore.then(expression)
                      .parseAll("(( 123 ) * 2 * 4 - 3 * ( 12 + 10 )) % 100").value);
code.dump();
System.println(code.run());

C'est également la sortie sous Linux.

       0:   53                                          push rbx
       1:   41 57                                       push r15
       3:   41 56                                       push r14
       5:   48 8b df                                    mov rbx, rdi
       8:   4c 8b fe                                    mov r15, rsi
       b:   4c 8b f2                                    mov r14, rdx
       e:   48 83 ec 10                                 sub rsp, 0x10
      12:   48 c7 c7 7b 00 00 00                        mov rdi, 0x7b
      19:   48 c7 c1 02 00 00 00                        mov rcx, 0x2
      20:   49 89 f8                                    mov r8, rdi
      23:   4c 0f af c1                                 imul r8, rcx
      27:   48 c7 c7 04 00 00 00                        mov rdi, 0x4
      2e:   4c 89 c1                                    mov rcx, r8
      31:   48 0f af cf                                 imul rcx, rdi
      35:   48 c7 c7 03 00 00 00                        mov rdi, 0x3
      3c:   49 c7 c0 0c 00 00 00                        mov r8, 0xc
      43:   49 c7 c3 0a 00 00 00                        mov r11, 0xa
      4a:   4b 8d 1c 18                                 lea rbx, [r8+r11]
      4e:   49 89 f8                                    mov r8, rdi
      51:   4c 0f af c3                                 imul r8, rbx
      55:   48 89 cf                                    mov rdi, rcx
      58:   49 2b f8                                    sub rdi, r8
      5b:   48 c7 c1 64 00 00 00                        mov rcx, 0x64
      62:   48 89 f8                                    mov rax, rdi
      65:   48 89 ce                                    mov rsi, rcx
      68:   48 99                                       cqo
      6a:   48 f7 fe                                    idiv rsi
      6d:   48 89 d6                                    mov rsi, rdx
      70:   49 89 f0                                    mov r8, rsi
      73:   4c 89 c0                                    mov rax, r8
      76:   48 83 c4 10                                 add rsp, 0x10
      7a:   41 5e                                       pop r14
      7c:   41 5f                                       pop r15
      7e:   5b                                          pop rbx
      7f:   c3                                          ret
18

Il y a aussi des résultats de calcul!

en conclusion

Normalement, le système de langage de script convertit AST en son propre code de format intermédiaire (code d'octet) et l'exécute. Il en va de même pour Kinx. Cependant, ce que vous faites n'est pas très différent de ce qui précède. C'est plus comme faire quelque chose de similaire à Compiler et l'exécuter dans votre propre ʻInterpreter. Si vous pouvez convertir en code de langage machine natif, ʻIntrpreter n'est pas nécessaire. Il traite généralement des opérations de longueur et des objets de classe multiples, et les langages typés dynamiquement ne connaissent pas le type avant l'exécution, donc c'est juste un tracas à compiler en langage machine. En particulier, si vous souhaitez prendre en charge x64 (Windows / Linux) / ARM et ainsi de suite, vous devez créer un compilateur pour chacun. Le codage par octets semble donc être la meilleure pratique.

Cela dit, JIT fait de son mieux pour les produits sérieux. Kinx peut également être partiellement converti en JIT avec des fonctions natives. De plus, comme il est réalisé dans la plage qui peut être exprimée par l'assembleur abstrait (SLJIT), il devrait être hautement portable vers diverses plates-formes.

Dans tous les cas, JIT a toujours été un sujet brûlant dans le traitement du langage, alors j'ai pensé que la bibliothèque JIT de Kinx serait un petit jouet amusant. Je te loue. De plus, comme j'ai travaillé dur cette fois pour implémenter un combinateur d'analyseur, je pense que je peux facilement créer mon propre DSL. Je pense que c'est assez amusant, mais qu'en est-il?

Le slogan de la bibliothèque Kinx JIT est ** "N'hésitez pas à JIT" **, n'est-ce pas?

Alors la prochaine fois.

Recommended Posts

Combinateur bibliothèque-analyseur Kinx (partie 1)
Combinateur bibliothèque-analyseur Kinx (partie 2)
Combinateur bibliothèque-analyseur Kinx (édition supplémentaire)