Rustのparserを試す

ちょっと必要になったきがするのでparserをかくためのものを探したり
サンプルを書いてみたりした。
個人的なメモ
What is the difference between parser generators and parser combinators? - Quora
compiler construction - What is the difference between LALR and LR parsing? - Stack Overflow
専門と離れているので適当に下調べ。

nom - Rust
combine - Rust
pest. The Elegant Parser
LALRPOP -
syntax-cli - npm

最初はlex(情報がたくさん),bison(情報がたくさん)に似てると書いてある
syntax-cliがいいかもしれないと思ったけど、
パーサーを生成するコマンドがbuild.rsでうまく自動化できなかったのでやめた。

結局最初から生成もセットで提供されている
lalrpopを使うことにした。

とりあえず式をパースするサンプルを書いた。
f:id:katakanan:20190706151544p:plain

use std::str::FromStr;
use crate::ast::Node;

grammar;

NUM_INTEGER: i32 = r"[0-9]+" => i32::from_str(<>).unwrap();
IDENTIFIER: String = r"[a-zA-Z_][a-zA-Z0-9_$]*" => <>.to_string();

pub Expr: Node = {
  <e:Expr> "+" <f:Factor> => Node::Binary{
                                            op: "+",
                                            left: Box::new(e),
                                            right: Box::new(f),
                                          },
  Factor,
};

pub Factor: Node = {
  <f:Factor> "*" <t:Term> => Node::Binary{
                                            op: "*",
                                            left: Box::new(f),
                                            right: Box::new(t),
                                          },
  Term,
};

pub Term: Node = {
  <n:NUM_INTEGER> => Node::Literal(n),
  <ident:IDENTIFIER> => Node::Ident(ident),
  "(" <Expr> ")",
};
#[derive(Debug)]
pub enum Node {
    Literal(i32),
    Ident(String),
    Binary {
        op: &'static str,
        left: Box<Node>,
        right: Box<Node>,
    },
}
#[macro_use]
extern crate lalrpop_util;

pub mod ast;
pub mod parser;

fn main() {
    println!("Hello, world!");
    println!("{:?}", parser::ExprParser::new().parse("((22))"));
    println!("{:?}", parser::ExprParser::new().parse("(a_A0)"));
    println!("{:?}", parser::ExprParser::new().parse("(2+a)*4"));
    println!("{:?}", parser::ExprParser::new().parse("2+a*4"));
    println!("{:?}", parser::ExprParser::new().parse("5*4+b"));
    println!("{:?}", parser::ExprParser::new().parse("5*(4+b)"));
}
    Finished dev [unoptimized + debuginfo] target(s) in 1.68s
     Running `target/debug/calc3`
Hello, world!
Ok(Literal(22))
Ok(Ident("a_A0"))
Ok(Binary { op: "*", left: Binary { op: "+", left: Literal(2), right: Ident("a") }, right: Literal(4) })
Ok(Binary { op: "+", left: Literal(2), right: Binary { op: "*", left: Ident("a"), right: Literal(4) } })
Ok(Binary { op: "+", left: Binary { op: "*", left: Literal(5), right: Literal(4) }, right: Ident("b") })
Ok(Binary { op: "*", left: Literal(5), right: Binary { op: "+", left: Literal(4), right: Ident("b") } })

うーんよさそう?

ある文法をパースしたかったら
既存のプロジェクトを参考にできる
GitHub - RustPython/RustPython: A Python Interpreter written in Rust
GitHub - tcr/rust-verilog: Verilog parsing and generator crate.

diagram-js customize 6. CustomizeRenderer

今回はBase Renderを継承した自作クラスに切り替える

bpmn-jsを参考にすると、lib/drawに主な3つのファイルがあるので
それを作る。(今回はまだMyRenderUtilは使わない)
f:id:katakanan:20190629234033p:plain

四角をつなげる線を引いたりするのに
lib/util/RenderUtilが必要で、
diagram-jsからそれを含めてコピーした。
f:id:katakanan:20190629234431p:plain

bpmn-jsのBPMNRendererを参考にdiagram-exampleと同じような動作になるように
必要な関数だけを実装した。

import {
    componentsToPath,
    createLine
  } from '../util/RenderUtil';
  
  import {
    append as svgAppend,
    attr as svgAttr,
    create as svgCreate
  } from 'tiny-svg';
  
  // apply default renderer with lowest possible priority
  // so that it only kicks in if noone else could render
  var HIGH_PRIORITY = 1500;
  import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer';
  import { pick } from 'min-dash';
  
  export default class MyRenderer extends BaseRenderer {
      constructor(eventBus, styles){
          super(eventBus, HIGH_PRIORITY);
          this.CONNECTION_STYLE = styles.style([ 'no-fill' ], { strokeWidth: 5, stroke: '#000' });
          this.SHAPE_STYLE = styles.style({ fill: 'white', stroke: '#000', strokeWidth: 2 });
      }
  
      canRender(element){
          return true;
      }
  
      drawShape(visuals, element){
        var rect = svgCreate('rect');
        svgAttr(rect, {
          x: 0,
          y: 0,
          width: element.width || 0,
          height: element.height || 0
        });
        svgAttr(rect, this.SHAPE_STYLE);
      
        svgAppend(visuals, rect);

        return rect;
      }
  
      drawConnection(visuals, connection){
          var type = connection.type;
 
          var line = createLine(connection.waypoints, this.CONNECTION_STYLE);
  
          svgAppend(visuals, line);
          return line;
      }
}
  
  MyRenderer.$inject = [
      'eventBus',
      'styles' 
  ];

適当なコミットの時点のものをコピーしてきたので
もしかしたら使ってないメンバ関数もあるかもしれない。
そしてここで、drawShape関数がcanvasに追加する
svgタグの要素を生成しており、svgAttrでスタイル(線の色とか)を決定している。
最後にsvgAppendで親(visuals)に対する子要素として追加される。

e-tipsmemo.hatenablog.com
の記事の2の方法。

import MyRenderer from './MyRenderer';

export default {
  __init__: [ 'MyRenderer' ],
  MyRenderer: [ 'type', MyRenderer ],
};

index.jsはいつも通り

Renderができたので、
Modeler.jsにモジュールを追加する。

import DrawModule from './lib/draw';
....
export default class Modeler extends Viewer{
    constructor(options){
        var options = {
            canvas:{
                container: options.container
            },
            modules:[
                .....
                DrawModule,
                .....
            ]
        };
        super(options);
    }
}

そしてapp.jsのスタイルを上書きするところはもういらない

// defaultRenderer.CONNECTION_STYLE = styles.style([ 'no-fill' ], { strokeWidth: 5, stroke: '#000' });
// defaultRenderer.SHAPE_STYLE = styles.style({ fill: 'white', stroke: '#000', strokeWidth: 2 });

見た目は変わらない。
f:id:katakanan:20190629235554p:plain

diagram-js customize 5. ContextPadProvider

e-tipsmemo.hatenablog.com

つぎはContextPadProvider
f:id:katakanan:20190622010224p:plain
といっても今回もソースコードを分離する程度

f:id:katakanan:20190622010429p:plain
ExampleContextPadProvider.jsの実装を/lib/features/context-pad/に移す。
ほぼコピペ

ContextPadProvider.js

import {
    assign,
    forEach,
    isArray
} from 'min-dash';

export default class ContextPadProvider{
    constructor(connect, contextPad, modeling){


        this._connect = connect;
        this._modeling = modeling;

        contextPad.registerProvider(this);
    }

    getContextPadEntries(element){
        var modeling = this._modeling;
        var connect = this._connect;

        var actions = {};

        function removeElement(){
            modeling.removeElements([ element ]);
        }
    
        function startConnect(event, element, autoActivate) {
            connect.start(event, element, autoActivate);
        }

        assign(actions, {
            'delete': {
                group: 'edit',
                className: 'context-pad-icon-remove',
                title: 'Remove',
                action: {
                  click: removeElement,
                  dragstart: removeElement
                }
            },
            'connect': {
                group: 'edit',
                className: 'context-pad-icon-connect',
                title: 'Connect',
                action: {
                  click: startConnect,
                  dragstart: startConnect
                }
            }
        });

        return actions;

    }

}

ContextPadProvider.$inject = [
    'connect',
    'contextPad',
    'modeling'
];

index.js

import DirectEditingModule from 'diagram-js-direct-editing';
import ContextPadModule from 'diagram-js/lib/features/context-pad';
import SelectionModule from 'diagram-js/lib/features/selection';
import ConnectModule from 'diagram-js/lib/features/connect';
import CreateModule from 'diagram-js/lib/features/create';

import ContextPadProvider from './ContextPadProvider';

export default {
  __depends__: [
    DirectEditingModule,
    ContextPadModule,
    SelectionModule,
    ConnectModule,
    CreateModule,
  ],
  __init__: [ 'contextPadProvider' ],
  contextPadProvider: [ 'type', ContextPadProvider ]
};

本当はこのすべてに依存しているわけではなさそうだが、とりあえず動いているのでヨシ(?)

diagram-js customize 4. PaletteProvider

次にPaletteProvider
f:id:katakanan:20190615153523p:plain

bpmn-jsを習って、
app/lib/features/palette
を以下のように構成。
f:id:katakanan:20190615161552p:plain

index.jsはappからimportするときの読み込まれる?
jsでファイルを分割するときの手法っぽい

import PaletteModule from 'diagram-js/lib/features/palette';
import CreateModule from 'diagram-js/lib/features/create';
import SpaceToolModule from 'diagram-js/lib/features/space-tool';
import LassoToolModule from 'diagram-js/lib/features/lasso-tool';
import HandToolModule from 'diagram-js/lib/features/hand-tool';
import GlobalConnectModule from 'diagram-js/lib/features/global-connect';
import translate from 'diagram-js/lib/i18n/translate';

import PaletteProvider from './PaletteProvider';

export default {
  __depends__: [
    PaletteModule,
    CreateModule,
    SpaceToolModule,
    LassoToolModule,
    HandToolModule,
    GlobalConnectModule,
    translate
  ],
  __init__: [ 'paletteProvider' ],
  paletteProvider: [ 'type', PaletteProvider ]
};

PaletteProvider.js
bpmn-jsはclass構文を使っていないが、こっちはできるだけこの構文を使っていくつもり。
export defaultがないと外部ファイルから使えないらしい

import { assign } from "min-dash";

export default class PaletteProvider{
    constructor(palette, create, elementFactory, spaceTool, lassoTool, handTool, globalConnect, translate){
        this._create = create;
        this._elementFactory = elementFactory;
        this._lassoTool = lassoTool;
        this._palette = palette;
        this._globalConnect = globalConnect;
        this._translate = translate;
        this._spaceTool = spaceTool;
        this._handTool = handTool;
        palette.registerProvider(this);
    }

    getPaletteEntries(){
        var actions = {};
        var create = this._create,
            elementFactory = this._elementFactory,
            lassoTool = this._lassoTool;
      
            assign(actions, {
                'lasso-tool':{
                    group: 'tools',
                    className: 'palette-icon-lasso-tool',
                    title: 'Active Lasso Tool',
                    action: {
                        click: function(event){
                            lassoTool.activateSelection(event);
                        }
                    }
                },
                'tool-separator': {
                    group: 'tools',
                    separator: true
                },
                'create-shape': {
                    group: 'create',
                    className: 'palette-icon-create-shape',
                    title: 'Create Shape',
                    action: {
                      click: function() {
                        var shape = elementFactory.createShape({
                          width: 200,
                          height: 80
                        });
              
                        create.start(event, shape);
                      }
                    }
                }
            });
    
            return actions;
      };

};

PaletteProvider.$inject = [
    'palette',
    'create',
    'elementFactory',
    'spaceTool',
    'lassoTool',
    'handTool',
    'globalConnect',
    'translate'
  ];

registerProviderはdiagram-js/palette.jsの関数を呼び出す。
初期化時にgetPaletteEntriesが呼ばれる。
ここはdiagram-js-exampleのExamplePaletteProvider.jsをコピーして
あとで拡張性が高そうなbpmn-jsの書き方に寄せた。

このdiagram-js関連のプログラムはすべてdependency injectionという設計で行われているらしく
クラスを拡張してコンストラクタの引数の順番で
必要なモジュールを構文で指定する必要があるのだと思う。

// constructor(
//     palette,
//     create,
//     elementFactory,
//     spaceTool,
//     lassoTool,
//     handTool,
//     globalConnect,
//     translate)

PaletteProvider.$inject = [
    'palette',
    'create',
    'elementFactory',
    'spaceTool',
    'lassoTool',
    'handTool',
    'globalConnect',
    'translate'
];

最後にModeler.jsで追加しているModuleを自作のほうに切り替える。

// import PaletteModule from 'diagram-js/lib/features/palette'; // palette
//import ExamplePaletteProvider from './ExamplePaletteProvider';
import PaletteModule from './lib/features/palette'

ここに自分オリジナルの形を追加していくのはelementFactoryやRenderが必要になってくる
いまのところはpalette-icon-create-shapeのwidthとheightを変更できることを確認する。

f:id:katakanan:20190615162916p:plain

diagram-js customize 3. CONNECTION/SHAPE_STYLE

e-tipsmemo.hatenablog.com
まず、前回スタイルがピンク色だったが、ここの色を変えるには

1.BaseRenderのスタイルを上書き
2.app/lib/draw/Renderに新しくdiagram-js/BaseRenderを継承したRender Classをつくっていろいろとやる。

とあるが、2は今は面倒なので(あとで追加する)
1の方法をとる

diagram-jsを継承したModeler ClassにはRender Classを得る関数があるのでそれを使う。
app.jsに追記

var defaultRenderer = modeler.get('defaultRenderer');
var styles = modeler.get('styles');
// override default stroke color
defaultRenderer.CONNECTION_STYLE = styles.style([ 'no-fill' ], { strokeWidth: 5, stroke: '#000' });
defaultRenderer.SHAPE_STYLE = styles.style({ fill: 'white', stroke: '#000', strokeWidth: 2 });

f:id:katakanan:20190615153309p:plain
黒になった。

次はPaletteProvider

diagram-js customize 2. Viewer

e-tipsmemo.hatenablog.com
Viewer.js

import Diagram from 'diagram-js';

import {
  assign,
  find,
  isFunction,
  isNumber,
  omit
} from 'min-dash';

import {
  domify,
  query as domQuery,
  remove as domRemove
} from 'min-dom';

var DEFAULT_OPTIONS = {
    width: '100%',
    height: '100%',
    position: 'relative'
};

export default class Viewer{// extends Diagram{
  constructor(options){
    var parentcontainer = options['canvas'].container;
    this.container = this.createContainer(options);
    this.attachTo(parentcontainer);
    var baseModules = options.modules || this.getModules();
    var additionalModules = options.additionalModules || [];
    var diagramModules = [].concat(baseModules, additionalModules);

    var diagramOption = {
      canvas: {container: this.container},
      modules: diagramModules
    };
   
    Diagram.call(this ,diagramOption);
  }

  createContainer(options){
    var container = domify('<div class="js-container"></div>');

    assign(container.style, {
      width: ensureUnit(DEFAULT_OPTIONS.width),
      height: ensureUnit(DEFAULT_OPTIONS.height),
      position: DEFAULT_OPTIONS.position
    });

    return container;
  }

  getModules(){
    return this.modules;
  }

  attachTo(parentNode){
    if(!parentNode){
      throw new Error('parenetNode required');
    }

    if (typeof parentNode === 'string') {
      parentNode = domQuery(parentNode);
    }

    if(parentNode.childNodes.length !== 0)
    {
      parentNode.removeChild(parentNode.childNodes[0]);
    }

    parentNode.appendChild(this.container);
  }

  destroy(){ Diagram.destroy(); }
  clear(){ Diagram.clear(); }

}

function ensureUnit(val) {
  return val + (isNumber(val) ? 'px' : '');
}

ここではdiagramを呼び出すときにOptionを適切な形で渡すためにいろいろとコンストラクタで行っている。
なぜ extends Diagramをしていないかというと
jsでclassを利用して継承を行うと、コンストラクタの中でsuperを呼ぶ前に、thisを利用してメンバ関数(実際はprototype?の関数?)を使えないらしいので、callによってそれと同じような挙動になるようにした。

コンストラクタの中では指定されたキャンバスの中にもう一個js-containerを作成。
DEFAULT_OPTIONを指定。
ここはbpmn-jsのViewerと同じ(はず)
moduleの配列をconcatして、diagram.callに渡す。
これもbpmn-jsのものを抜き出した。

index.htmlはそのままでapp.js, Modeler.js, Viewer.jsを作れば
これでdiagram-js-exampleを再現できるはず。。

でもスタイルが適用されていないので、diagram-js.cssのスタイルのデフォルトのピンク
f:id:katakanan:20190615152220p:plain

diagram-js customize 1. container

e-tipsmemo.hatenablog.com
前回はリファレンスとなるbpmn-js-exampleを実行できるようになったので
今度はdiagram-jsのexampleに機能を追加する。

f:id:katakanan:20190609190445p:plain
e-tipsmemo.hatenablog.com
public 以下のindex.htmlを必要に応じてbpmn-jsに寄せた。

<html>
  <head>
    <link rel="stylesheet" href="css/diagram-js.css">
    <link rel="stylesheet" href="css/app.css" />
    <style>
      .palette-icon-lasso-tool {
        background: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%221.5%22%20stroke-dasharray%3D%225%2C%205%22%20width%3D%2246%22%20height%3D%2246%22%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%2226%22%20height%3D%2226%22%2F%3E%3C%2Fsvg%3E');
      }

      .palette-icon-create-shape {
        background: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%221.5%22%20width%3D%2246%22%20height%3D%2246%22%3E%3Crect%20x%3D%2210%22%20y%3D%2213%22%20width%3D%2226%22%20height%3D%2220%22%2F%3E%3C%2Fsvg%3E');
      }

      .context-pad-icon-remove {
        background: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%221.5%22%20width%3D%2246%22%20height%3D%2246%22%3E%3Cline%20x1%3D%225%22%20y1%3D%225%22%20x2%3D%2215%22%20y2%3D%2215%22%2F%3E%3Cline%20x1%3D%2215%22%20y1%3D%225%22%20x2%3D%225%22%20y2%3D%2215%22%2F%3E%3C%2Fsvg%3E') !important;
      }

      .context-pad-icon-connect {
        background: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22%23000%22%20stroke-width%3D%221.5%22%20width%3D%2246%22%20height%3D%2246%22%3E%3Cline%20x1%3D%2215%22%20y1%3D%225%22%20x2%3D%225%22%20y2%3D%2215%22%2F%3E%3C%2Fsvg%3E') !important;
      }
    </style>
  </head>
  <body>
    <div id="container"></div>
    <script src="./js/bundle-app.js"></script>
  </body>
</html>

cssはbpmn-jsのものをコピー
追加のスタイルはPaletteProvider部分にあるアイコンをSVGでそのまま書いてあるのでコピペ
divが実際にdiagramが描かれる要素の親になる。
scriptはwebpackが吐き出すjsを指定する。

diagram-jsのサンプルはappがdiagram classをインスタンス化してモジュールなどを追加していたが、
bpmn-jsに倣って、
Modeler class → Viewer class → diagram classと継承していって、
app.jsからはModeler classのインスタンスを作るときに、diagramの親要素を指定するだけにする。

app,js

import Modeler from './Modeler';

var modeler  = new Modeler({
  container: '#container',  
});

Modeler.js

import Viewer from './Viewer';

import SelectionModule from 'diagram-js/lib/features/selection'; // select elements
import ZoomScrollModule from 'diagram-js/lib/navigation/zoomscroll'; // zoom canvas
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'; // scroll canvas
import ModelingModule from 'diagram-js/lib/features/modeling'; // basic modeling (create/move/remove shapes/connections)
import MoveModule from 'diagram-js/lib/features/move'; // move shapes
import OutlineModule from 'diagram-js/lib/features/outline'; // show element outlines
import LassoToolModule from 'diagram-js/lib/features/lasso-tool'; // lasso tool for element selection
import PaletteModule from 'diagram-js/lib/features/palette'; // palette
import CreateModule from 'diagram-js/lib/features/create'; // create elements
import ContextPadModule from 'diagram-js/lib/features/context-pad'; // context pad for elements,
import ConnectModule from 'diagram-js/lib/features/connect'; // connect elements
import RulesModule from 'diagram-js/lib/features/rules'; // rules

import ExampleContextPadProvider from './ExampleContextPadProvider';
import ExamplePaletteProvider from './ExamplePaletteProvider';
import ExampleRuleProvider from './ExampleRuleProvider';

var ExampleModule = {
    __init__: [
      'exampleContextPadProvider',
      'examplePaletteProvider',
      'exampleRuleProvider'
    ],
    exampleContextPadProvider: [ 'type', ExampleContextPadProvider ],
    examplePaletteProvider: [ 'type', ExamplePaletteProvider ],
    exampleRuleProvider: [ 'type', ExampleRuleProvider ]
};

export default class Modeler extends Viewer{
    constructor(options){
        var options = {
            canvas:{
                container: options.container
            },
            modules:[
                SelectionModule,
                ZoomScrollModule,
                MoveCanvasModule,
                ModelingModule,
                MoveModule,
                OutlineModule,
                LassoToolModule,
                PaletteModule,
                CreateModule,
                ContextPadModule,
                ConnectModule,
                RulesModule,
                ExampleModule
            ]
        };
        super(options);
    }
}

必要なモジュールはdiagram-js exampleのapp.jsをそのまま持ってくる。
つぎはViewer.jsについて