LoginSignup
7
3

More than 5 years have passed since last update.

Nodeでトランスパイラを作ろうとして失敗した話、と思ったら復活した。

Last updated at Posted at 2017-07-13

この記事を書くきっかけ

JavaScriptのライブラリにmathjsという数学関連ライブラリがある。これは複素数や行列演算などをサポートしいるようなので使ってみようと思ったが罠があった。
Numberは普通に使えるが複素数クラスにするとObjectになり四則演算を使うと、多分toString()が呼ばれ文字列として処理される。途中に数字を混ぜるとNaNになってしまう。気づくまで小一時間かかった...orz。
mathjsadd,subtracut,multiply,divideなどを使えば回避できるが計算部分が長くなる上可読性は下がるしバグの要因にもなり得る...。

diff_number.js
export default function(func, x, delta){
    if( !delta ) delta=1.0e-8;
    return (func(x+0.5*delta)-func(x-0.5*delta))/delta;
}

export function forward(func, x, delta){
    if( !delta ) delta=1.0e-8;
    return (func(x+delta)-func(x))/delta;
}

export function forward(func, x, delta){
    if( !delta ) delta=1.0e-8;
    return (func(x)-func(x-delta))/delta;
}

これが

js_math.js
import math from 'mathjs'

export default function(func, x, delta){
    if( !delta ) delta=1.0e-8;
    return math.divide(math.subtract(func(math.add(x, math.multiply(0.5, delta))), func(math.subtract(x, math.multiply(0.5, delta)))), delta);
}

export function forward(func, x, delta){
    if( !delta ) delta=1.0e-8;
    return math.divide(math.subtract(func(math.add(x, delta)),func(x)), delta);
}

export function backward(func, x, delta){
    if( !delta ) delta=1.0e-8;
    return math.divide(math.subtract(func(x).func(math.subtract(x, delta))), delta);
}

こうなる。かなり苦しい実装である。
JavaScriptはトランスパイルをさせるのでそこに+-*/の四則演算周りをmathクラスに置き換えてしまおうと考えた。
現在、作業中だがNodeとクライアントのJavaScriptだとかなり書き味が違ったのでその点とトランスパイラの実装をメモとして残す。
必要な知識などはより高度なコンパイラで詳しい説明を@maekawatoshikiさんがコンパイラ作り入門でしてくれるのでここでは実装に重きを置きます。

main部分

mybabel
#!/usr/bin/env node
// -*- coding:utf-8 mode:javascript -*-
const Argv=require('./node_scripts/argparse.js');
const fs=require('fs');
const path=require('path');
const token=require('./node_scripts/token_util.js');
const keyword=require('./node_scripts/js_keyword.js');
const srcParser=require('./node_scripts/source_parser.js');

(function(){
    console.log('===== mybabel START =====');
    const argv=new Argv(process.argv);
    if( argv.inputs.length!==1 ) throw new Error('please input only 1 file');
    const text=fs.readFileSync(argv.inputs[0], 'utf-8');
    const tokens=token.decompose(text);
//    token.checkScope(tokens); // バグがあるのでコメントアウト
// 以下、静的解析(遊び)
    console.log("n tokens : "+tokens.length);
    console.log("nest depth : "+token.nestDepth(tokens));
    console.log("n keyword : "+keyword.n(tokens));
    console.log("n + : "+token.nToken('+', tokens));
    console.log("n - : "+token.nToken('-', tokens));
    console.log("n / : "+token.nToken('/', tokens));
    console.log("n * : "+token.nToken('*', tokens));

    const src_obj=srcParser.parse(tokens);

    console.log('===== mybabel FINISH =====');
})();

コマンドラインツールなので拡張子は取り除く。私の環境の馬鹿なエディタ用に// -*- coding:utf-8 mode:javascript -*-でJavaScriptコードだと教えています。
FetchもXMLHTTPRequestもブラウザAPIなのでfsでファイル操作する。(どんだけブラウザとNodeは仲が悪いんだと思った、それぐらい対応させておけば...、requireを使わされるのも、importになれた身としては辛い)
メイン部分は即時関数で囲んでいます。明示的に実行しないと何もせずにプロセスが終わる。Windowロード時に実行させるブラウザ仕様に慣れていると一瞬忘れる。
argv.jsPythonのargparseを真似て作った...が具体的な実装は今は殆ど無い。
誰かpythonのメイン関数

if __name__=="__main__":

みたいな書き方知りませんか?

トークン解析

トークン分解

正規表現でやらせる。

token_decompose
function decompose(text){
    const pattern=/(\/\*[\s\S]*?\*\/|\/{2,}[^\r\n]*(?:\r\n|\r|\n|)|"(?:\\[\s\S]|[^"\r\n\\])*"|'(?:\\[\s\S]|[^'\r\n\\])*'|(?:(?:\/(?!\*)(\?:\\.\|[^\/\r\n\\])+\/)(?:[gimy]{0,4}|\b)(?=\s*(?:(?!\s*[\/\\<>*+%`^"'`\w$-])[^\/\\<>*+%`^`'"@({[\w$-]|===?|!==?|\|\||[&][&]|\/[*\/]|[,.;:!?)}\]\\r\\n]|$)))|<([^\s>]*)[^>]*[\s\S]*?<\/\2>|=>|>>>=?|<<=|===|!==|>>=|\+\+(?=\+)|\-\-(?=\-)|[=!<>*+\/&|^-]=|[&][&]|\|\||\+\+|\-\-|<<|>>|0(?:[xX][0-9\a\-fA-F]+|[0-7]+)|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[1-9]\d*|[-+\/%*=&|^~<>!?:,;@()\\[\].{}]|(?![\r\n])\s+|(?:\r\n|\r|\n)|[^\s+\/%*=&|^~<>!?:,;@(\)\\\[\].{}'"-]+)/g;
    return tokens=text.match(pattern).filter((x)=>{return x.match(/\S/g); });
}

正規表現部分はコピペですアロー演算子=>だけ足しておきました。
ここが失敗でした、テンプレートリテラル、バックスラッシュで囲んだものに対応できない...。正規表現は可読性も低いし拡張性が著しく下がるので作り直します。
ちょっと探すと見つかったのでこのままで行きます。なかなかキーワードを検索して切ってを書くのも大変だった。変更部分は更新履歴で確認してください(汗。
ちょっと確認するとコメントアウトに対応していなかった。要改善

トークンの種類

キーワード

'if'や'while'、'return'等、特別の意味を持ったものでエディタなどでハイライトが付くもの。
ifは必ず(条件式)が来る、来なければエラーになる、またelseif節が無いとエラーになる(というかする)。という風に構文解析のヒントになる。
一方、functionは後ろに名前をつけてもいいし(){}で定義にもできる。名前(引数)で関数呼び出しとも取れるので難しい。
const,let,varなどの変数宣言、こいつらも後ろに変数の名前が必要である。

区切り子

区切り子はカッコとそれ以外に分ける。カッコ[]{}()は必ず対応する区切り文字で切らなければならないのでエラーの発見や意味のある文の塊に分ける際に役に立つ。;は文を区切る際に使う。,const i=0,j=0;と文を区切らない。

演算子

今回の目的ではあるがトークン分析の段階ではあまり役に立たないかな。+-/*などは左辺と右辺が必要である値を返す。C++で演算子のオーバーロードとかするとよくわかる(というかおんなじ機能がJavaScriptにあれば:sob:)。
++--演算子は右辺は必要ない。
というかJavaScriptのトランスパイルでは文字列を文字列にするので返り値とか気にする必要はない。

トークン分析

分析用関数をいくつか作ってみた。

checkScope.js
function checkScope(tokens){
    let pos=0;
    const nest=[];
    while( pos<tokens.length ){
        if( tokens[pos]==='(' ) nest.push(')');
        else if( tokens[pos]==='{' ) nest.push('}');
        else if( tokens[pos]==='[' ) nest.push(']');

        if( tokens[pos]===')' && nest.pop()!==')' ) throw new Error('Block scope is not match ('+tokens[pos]+' '+nest.length);
        else if( tokens[pos]==='}' && nest.pop()!=='}' ) throw new Error('Block scope is not match {'+tokens[pos]+' '+nest.length);
        else if( tokens[pos]===']' && nest.pop()!==']' ) throw new Error('Block scope is not match ['+tokens[pos]+' '+nest.length);

        pos++;
    }
    if( nest.length!==0 ) throw new Error('Block scope is not closed '+nest.pop());
}

自分で作った小さなjsファイルだとエラーなく実行できて完成!と思ったら。npmのjqueryやthree等では軒並みエラーを出したのでどこかバグっている。調査中、多分トークン分解部分だと思うので作りなおすと治るかも

getScope.js
function getScope(tokens, pos){
    if( tokens[pos]!=='(' && tokens[pos]!=='{' && tokens[pos]!=='[' ) throw new Error('There is not block scope '+tokens[pos]);
    const start=pos;
    let nest=0;
    while( pos<tokens.length ){
        if( tokens[pos]==='(' || tokens[pos]==='{' || tokens[pos]==='[' ) nest++;
        if( tokens[pos]===')' || tokens[pos]==='}' || tokens[pos]===']' ){
            nest--;
            if( nest===0 ) return tokens.slice(start, pos+1);
        }
        pos++;
    }
}

カッコの始まりから終わりまでカッコはネストするので中身のカッコも取り出す。前のcheckScopeで対応が取れていると思っていたので対応チェックまではしなかった。要改良もはや全体の改良が必要なので瑣末な改良は後回しにする。

nestDepth.js
function nestDepth(tokens){
    let max_depth=0, nest=0, pos=0;
    while( pos<tokens.length ){
        if( tokens[pos]==='(' || tokens[pos]==='{' || tokens[pos]==='[' ){
            nest++;
            if( nest>max_depth ) max_depth=nest;
        }
        if( tokens[pos]===')' || tokens[pos]==='}' || tokens[pos]===']' ){
            nest--;
            if( nest<0 ) throw new Error("too many braket)}]");
        }
        pos++;
    }
    return max_depth;
}

ネストの深さは闇の深さということでネストの深さを測ってみた。自分のjsコードでだいたい9ぐらいでした(結構深かった)。
jquery(minifyしてない公開版)で20でした。バンドル後で規模を考えると意外と浅い気がします。

感想と今後

結局、他人のコードを丸パクリすると痛い目を見るということでした。一応それなりに動いているのでこれで少しずつ部品づくりをしながら、トークン分解部分を自分で作り直します。nodeでそれなりの速度が出てる。threeのフルコード解析でも一瞬なので速度的な実用は問題ありません。まあ世の中のトランスパイラがnode実装なのだから当然と言えば当然ですがw
いろいろエラーがありますが簡単な構文解析はできて他人のコードを解析させるのは楽しかったです。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3