*コンパイラになる方法-JavaScriptコンパイラの作成

こんにちは、Habr! 小坂真理子による記事「 How to be * a compiler-JavaScriptでコンパイラを作る 」の翻訳を紹介します



*そうです! コンパイラーであることは素晴らしいことです!



ブルックリンのブッシュウィックでの素晴らしい日曜日の午後でした。 私の地元の書店で、私はジョン・マエドの本「Design by Numbers」に出会った。 これは、コンピュータープログラミングの概念を視覚的に提示するために、MIT Media Labで90年代後半に作成されたプログラミング言語であるDBNを学習するための段階的な指示でした。



最初の図に示されている3行のコードは、白い紙に黒い線を描きます( ここから例を取り上げます )。 たとえば、正方形などのより複雑な形状を描画するには、さらに多くの線を描画するだけです(2番目の図)。



画像



2016年、これは興味深いプロジェクトになるとすぐに思いました-JavaをインストールせずにDBNからSVGを作成して、元のDBNコードを実行することです。



そのために、DBNからSVGにコンパイラーを作成する必要があると判断したため、コンパイラー作成の探求が始まりました。 コンパイラを作成することは、かなり複雑な科学的プロセスのように聞こえますが、インタビューでグラフ探索を書いたことすらありません...コンパイラを書くことはできますか?



画像



最初に自分でコンパイラになろう



コンパイラは、コードの一部を受け取り、それを別のコードに変換するメカニズムです。 簡単なDBNコードを物理的な図面にコンパイルしましょう。



3つのDBNコマンドを見てみましょう:Paperは紙の色を設定し、Penはブラシの色を設定し、Lineは線を描画します。 色100は100%黒に相当し、CSSではrgb(0%、0%、0%)です。 DBNで作成された画像は常にグレースケールです。 DBNでは、用紙は常に100×100、線幅は常に1、線自体は(x、y)座標で指定され、カウントは画像の左下隅から取られます。



これにこだわって、自分自身でコンパイラになろう。 紙、ペンを取り、次のコードを写真にコンパイルしてみてください。



Paper 0 Pen 100 Line 0 50 100 50
      
      





シートの左端から右に水平線を引きましたか?それは垂直に中央に配置されていますか? おめでとうございます! あなたはちょうどコンパイラになりました。



画像



コンパイラはどのように機能しますか?



私たちがコンパイラーだったときに頭の中で何が起こったのか見てみましょう。



1.字句解析(トークン化)



最初に行ったのは、ソースコードをスペースで単語に分割することでした。 このプロセスでは、「単語」や「番号」など、トークンごとにプリミティブ型を条件付きで決定しました。



画像



2.解析



テキストの断片をトークンに分割したらすぐに、それぞれを調べて、それらの間の関係を見つけようとしました。



この場合、一連の数字とそれらに関連する単語をグループ化しました。 これを行った後、特定のコード構造を区別し始めました。



画像



3.変換



解析後、結果の構造を最終結果により適したものに変換しました。 この場合、画像を描画します。つまり、構造を、人間が理解できる段階的な指示に変換する必要があります。



画像



4.コード生成



この段階では、描画の準備の前のステップで行った指示に従うだけです。



画像





これはコンパイラーが行うことです!



作成した図は、コンパイルされた結果です(Cプログラムのコンパイル中に作成される.exeファイルに似ています)。 この写真は、すべての人または任意のデバイス(スキャナー、カメラなど)に送信でき、誰もがシートの中央の黒い線を認識します。



コンパイラを書きましょう



コンパイラの動作がわかったので、別のスクリプトを作成しますが、JavaScriptを使用します。 このコンパイラは、DBNコードを取得してSVGに変換します。



1.レクサー機能



「I have a pen」という文を単語[I、I、have a pen]に分割できるのと同じ方法で、字句解析プログラムは、文字列として表されるコードを特定の意味のある部分(トークン)に分割できます。 DBNでは、すべてのトークンはスペースで区切られ、「単語」または「番号」として分類されます。



 function lexer (code) { return code.split(/\s+/) .filter(function (t) { return t.length > 0 }) .map(function (t) { return isNaN(t) ? {type: 'word', value: t} : {type: 'number', value: t} }) }
      
      





 input: "Paper 100" output:[ { type: "word", value: "Paper" }, { type: "number", value: 100 } ]
      
      





2.パーサー機能



パーサーは各トークンを通過し、構文情報を収集して、いわゆる抽象構文ツリーを構築します。 ASTはコードのマップとして見ることができます-ASTがどのように構成されているかを見る方法です。



コードには、「NumberLiteral」と「CallExpression」の2つの構文タイプがあります。 NumberLiteralは、値が数値であることを意味します。 この番号は、CallExpressionの引数として使用されます。



 function parser (tokens) { var AST = { type: 'Drawing', body: [] } //    while (tokens.length > 0){ var current_token = tokens.shift() //          , //     ,    if (current_token.type === 'word') { switch (current_token.value) { case 'Paper' : var expression = { type: 'CallExpression', name: 'Paper', arguments: [] } //     CallExpression  Paper, //       var argument = tokens.shift() if(argument.type === 'number') { expression.arguments.push({ //        type: 'NumberLiteral', value: argument.value }) AST.body.push(expression) //      } else { throw 'Paper command must be followed by a number.' } break case 'Pen' : ... case 'Line': ... } } } return AST }
      
      







 input: [ { type: "word", value: "Paper" }, { type: "number", value: 100 } ] output: { "type": "Drawing", "body": [{ "type": "CallExpression", "name": "Paper", "arguments": [{ "type": "NumberLiteral", "value": "100" }] }] }
      
      





3.トランス機能



前の手順で作成した抽象構文ツリー(AST)は、コードで何が起こるかをよく説明していますが、これからSVGを作成することはできません。



たとえば、「Paper」コマンドは、DBNで記述されたコードに対してのみ明確です。 SVGでは、紙を表すために<rect>要素を使用するため、ASTをSVGに適した別のASTに変換する関数が必要です。



 function transformer (ast) { var svg_ast = { tag : 'svg', attr: { width: 100, height: 100, viewBox: '0 0 100 100', xmlns: 'http://www.w3.org/2000/svg', version: '1.1' }, body:[] } var pen_color = 100 //    -  //    while (ast.body.length > 0) { var node = ast.body.shift() switch (node.name) { case 'Paper' : var paper_color = 100 - node.arguments[0].value svg_ast.body.push({ //    rect    svg_ast tag : 'rect', attr : { x: 0, y: 0, width: 100, height:100, fill: 'rgb(' + paper_color + '%,' + paper_color + '%,' + paper_color + '%)' } }) break case 'Pen': pen_color = 100 - node.arguments[0].value //       `pen_color` break case 'Line': ... } } return svg_ast }
      
      





 input: { "type": "Drawing", "body": [{ "type": "CallExpression", "name": "Paper", "arguments": [{ "type": "NumberLiteral", "value": "100" }] }] } output: { "tag": "svg", "attr": { "width": 100, "height": 100, "viewBox": "0 0 100 100", "xmlns": "http://www.w3.org/2000/svg", "version": "1.1" }, "body": [{ "tag": "rect", "attr": { "x": 0, "y": 0, "width": 100, "height": 100, "fill": "rgb(0%, 0%, 0%)" } }] }
      
      





4.ジェネレーター機能



コンパイラの最後のステップでは、前のステップで作成した新しいASTに基づいてSVGコードを構築する関数が呼び出されます。



 function generator (svg_ast) { //      // { "width": 100, "height": 100 }   'width="100" height="100"' function createAttrString (attr) { return Object.keys(attr).map(function (key){ return key + '="' + attr[key] + '"' }).join(' ') } //     <svg>.      svg  var svg_attr = createAttrString(svg_ast.attr) //      svg_ast  svg  var elements = svg_ast.body.map(function (node) { return '<' + node.tag + ' ' + createAttrString(node.attr) + '></' + node.tag + '>' }).join('\n\t') //      svg    SVG  return '<svg '+ svg_attr +'>\n' + elements + '\n</svg>' }
      
      





 input: { "tag": "svg", "attr": { "width": 100, "height": 100, "viewBox": "0 0 100 100", "xmlns": "http://www.w3.org/2000/svg", "version": "1.1" }, "body": [{ "tag": "rect", "attr": { "x": 0, "y": 0, "width": 100, "height": 100, "fill": "rgb(0%, 0%, 0%)" } }] } output: <svg width="100" height="100" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg"> <rect x="0" y="0" width="100" height="100" fill="rgb(0%, 0%, 0%)"> </rect> </svg>
      
      





5.すべてをまとめる



コンパイラを「sbnコンパイラ」と呼びましょう。 レクサー、パーサー、トランスフォーマー、ジェネレーターを使用してsbnオブジェクトを作成します。 これらのメソッドの4つのチェーンを呼び出す「コンパイル」メソッドを追加します。



これで、コード行をコンパイルメソッドに渡し、SVGを取得できます。



 var sbn = {} sbn.VERSION = '0.0.1' sbn.lexer = lexer sbn.parser = parser sbn.transformer = transformer sbn.generator = generator sbn.compile = function (code) { return this.generator(this.transformer(this.parser(this.lexer(code)))) } //  sbn  var code = 'Paper 0 Pen 100 Line 0 50 100 50' var svg = sbn.compile(code) document.body.innerHTML = svg
      
      





各ステップでコンパイラの結果を確認できるインタラクティブなデモを作成しまし 。 sbnコンパイラーのコードはgithubからダウンロードできます。 現在、コンパイラー機能の拡張に取り組んでいます。 実際にこの記事で作成された基本的なコンパイラーだけを見たい場合は、 ここで見つけることができます。



画像



コンパイラは再帰、トラバーサルなどを使用する必要がありますか?



はい、これらの手法はすべてコンパイラーの作成に最適ですが、コンパイラーにすぐに適用する必要があるという意味ではありません。



DBNコマンドの小さなセットのみを使用してコンパイラーを書き始めました。 徐々に機能が複雑になり始めたので、変数、ブロック、ループの使用をコンパイラーに追加します。 これらの構造をすべて持つことは確かに良いことですが、最初から適用する必要はありません。



コンパイラの作成は素晴らしい



コンパイラを書くことができたら何ができますか? JavaScriptの新しいバージョンをスペイン語で作成することもできます。EspañolScriptはどうですか?



 // ES (español script) función () { si (verdadero) { return «¡Hola!» } }
      
      





絵文字(絵文字コード)またはカラー画像(ピート)を使用して言語を書いた人はすでにいます。 可能性は無限です!



コンパイラー作成プロセスのトレーニング



コンパイラの作成は楽しかったし、ソフトウェア開発について多くのことを学びました。 コンパイラーを作成する過程で学んだことをいくつかリストします。



画像



1.何かを知らないのは普通のことです



字句解析プログラムのように、最初からすべてを知る必要はありません。 一部のコードまたは技術の一部をよく理解していない場合、これを次のステップに進めるのが普通です。 緊張しないでください、遅かれ早かれ、あなたはそれを理解するでしょう!



2.エラーメッセージのテキストに注意してください。



パーサーの役割は、厳密に指示に従い、すべてがルールに記載されているとおりに記述されているかどうかを確認することです。 はい、間違いは頻繁に発生します。 これが発生した場合は、有益でわかりやすいエラーメッセージを送信してみてください。 「だからうまくいかない」(JavaScriptの「ILLEGAL Token」または「undefined is a function」ではない)と言うのは簡単ですが、代わりにできるだけ多くの有用な情報をユーザーに提供しようとします。



これは、チームコミュニケーションにも当てはまります。 「はい、うまくいきません」と言うのではなく、誰かがタスクにこだわっているとき、「...などのキーワードでGoogleの情報を検索します」または「そのようなドキュメントを読むことをお勧めします」と言うことができます。 他の人の仕事を引き受ける必要はありませんが、彼に新鮮なアイデアを投げつけるだけで、彼の仕事をより良く、より速くするのを確実に助けることができます。



プログラミング言語Elm は、このアプローチ使用してエラーメッセージを出力します。ユーザーには、問題を解決するためのオプションが提供されます(「これを試してみたいですか?」)。



3.コンテキストがすべてです



最後に、トランスフォーマーが1つのタイプのASTを最終結果により適した別のASTに変えたように、プログラミングのすべてがコンテキストに依存します。



完全に完璧なソリューションはありません。 それが今流行しているという理由だけで、またはあなたが以前にそれをすでに行ったという理由だけで、何かをしないでください、最初にタスクのコンテキストについて考えてください。 あるユーザーにとってうまくいくものは、別のユーザーにとってはひどいものになる可能性があります。



したがって、「トランスフォーマー」の仕事に感謝します。チーム内のそのような人、よく完成した人、誰かが仕事を始めた人、またはリファクタリングをしている人をご存知でしょう。 本質的に新しいコードを作成するわけではありませんが、その作業の結果は最終的な高品質製品にとって非常に重要です。



この記事をお読みいただき、コンパイラーを作成して自分自身がコンパイラーであることの素晴らしさを納得させてください。



これは、コロンビアのメデリンで開催されたJSConf Colombia 2016でのプレゼンテーションの一部である小坂真理子の記事の翻訳です。 これについて詳しく知りたい場合は、スライドとこちらの元の記事をご覧ください



All Articles