Solidityで電卓を書いてみた

#はじめに
- Solidityを使って何か特殊なものを書いてみたいと思ったのが始まり
- PEGからの実装の練習として電卓を作ってみた
- 実装において、状態変数を使わないことで全てpureな関数としたためブロックチェーンには計算過程や結果の記録は一切行わない~~（ブロックチェーンの意味が全くないですね...）~~
    - 関数自体はブロックチェーンに記録されていて、パブリックに参照可能かつ変更不可であることにいくつかメリットがありそう
- 文字列で計算式を与えれば計算します (e.g. "12+ 3 *  4  " -> 24)
- スペーススキップあり、~~括弧は未実装~~(実装しました)
- [Remix](https://remix.ethereum.org/#optimize=false)で実行できますが、処理落ちでページが読み込めないことが稀によくあります

#PEG
```:PEG
Math
    <- AddSub
AddSub
    <- MulDiv (('+' / '-') MulDiv)*
MulDiv
    <- Num (('*' / '/') Num)*
Num
    <- Space* NumExpr Space*
NumExpr
    <- [0-9]+ / Bracket
Bracket
    <- '(' Math ')'
Space
    <- [ ]
```

#実装
```js:math.sol
pragma solidity ^0.4.24;
contract Main {
    
    function math(string _input) external pure returns(string,int) {
        bytes memory stringBytes = bytes(_input);
        (int answer,) = addsub(stringBytes,0);
        return (_input,answer);
    }
    function addsub(bytes _input, uint pc) internal pure returns(int, uint){
        (int leftNum, uint leftRemain) = muldiv(_input,pc);
        if(leftRemain >= _input.length)return (leftNum, leftRemain);
        int rightNum;
        uint rightRemain = leftRemain;
        int sum = leftNum;
        while(rightRemain < _input.length) {
            if(_input[rightRemain] == 0x2b){
                (rightNum,rightRemain) = muldiv(_input,rightRemain+1);
                sum += rightNum;
            } else if(_input[rightRemain] == 0x2d) {
                (rightNum,rightRemain) = muldiv(_input,rightRemain+1);
                sum -= rightNum;
            } else {
                break;
            }
        }
        return (sum, rightRemain);
    }
    function muldiv(bytes _input, uint pc) internal pure returns(int,uint){
        (int leftNum, uint leftRemain) = num(_input,pc);
        if(leftRemain >= _input.length)return (leftNum, leftRemain);
        int rightNum;
        uint rightRemain = leftRemain;
        int sum = leftNum;
        while(rightRemain < _input.length) {
            if(_input[rightRemain] == 0x2a){
                (rightNum,rightRemain) = num(_input,rightRemain+1);
                sum *= rightNum;
            } else if(_input[rightRemain] == 0x2f) {
                (rightNum,rightRemain) = num(_input,rightRemain+1);
                sum /= rightNum;
            } else {
                break;
            }
        }
        return (sum, rightRemain);
    }
    function num(bytes _input, uint pc) internal pure returns(int, uint){
        pc = spaceSkip(_input,pc);
        
        if(_input[pc] == 0x28){
            (int innerAns, uint innerOut) = addsub(_input,pc+1);
            innerOut = spaceSkip(_input, innerOut);
            require(_input[innerOut] == 0x29);
            innerOut = spaceSkip(_input, innerOut+1);
            return (innerAns, innerOut);
        }
        
        uint start = pc;
        while(pc < _input.length && isNum(_input[pc])){
            pc++;
        }
        uint remain = pc;
        bytes memory temp = selectedBytes(_input,start,remain);
        int thisNum = bytesToInteger(temp);
        remain = spaceSkip(_input,remain);
        return (thisNum, remain);
    }
    function spaceSkip(bytes memory _input, uint pc) internal pure returns(uint){
        while(pc < _input.length && _input[pc] == 0x20){
            pc++;
        }
        return pc;
    }
    function selectedBytes(bytes memory _input, uint start, uint stop) internal pure returns(bytes){
        bytes memory temp = new bytes(stop-start);
        for(uint i=start;i<stop;i++){
            temp[i-start] = _input[i];
        }
        return temp;
    }
    function isNum(byte b) internal pure returns(bool){
        if(0x30<=uint(b) && uint(b)<=0x39)return true;
        else return false;
    }
    function stringToInteger(string memory _str) internal pure returns(int){
        bytes memory temp = bytes(_str);
        return bytesToInteger(temp);
    }
    function bytesToInteger(bytes memory _str) internal pure returns(int){
        uint strLen = _str.length;
        uint tempAns = 0;
        for(uint i=strLen;i>0;i--){
            tempAns += bytesToUnsignedInteger(_str,strLen-i) * 10**(i-1);
        }
        return int(tempAns);
    }
    function bytesToUnsignedInteger(bytes memory strByte, uint index) internal pure returns(uint){
        byte char = strByte[index];
        if     (char == 0x30) return 0;
        else if(char == 0x31) return 1;
        else if(char == 0x32) return 2;
        else if(char == 0x33) return 3;
        else if(char == 0x34) return 4;
        else if(char == 0x35) return 5;
        else if(char == 0x36) return 6;
        else if(char == 0x37) return 7;
        else if(char == 0x38) return 8;
        else if(char == 0x39) return 9;
        else revert("Can't parse byte to uint");
    }
    function stringToBytes(string memory str) internal pure returns(bytes){
        return bytes(str);
    }
}
```

#Remixでの実行方法
- Remixは[こちら](https://remix.ethereum.org/#optimize=false)
- Remixの中央列上側のコードウィンドウに上記コードを貼り付けます
- 右列のCompileタブにて"Start to compile"をクリックしコンパイルします
- Runタブに切り替えてEnvironmentが"Javascript VM"になっていることを確認し、中央あたりにある赤い"Deploy"ボタンをクリックしデプロイします
- 下側のDeployed Contractsに追加された"Main at 0x~"をクリックするとmathと書かれた青いボタンとその横にテキストボックスが出ると思います
- テキストボックスに例えば`"(2 + 3) * 4"`と入力しmathボタンをクリックすると、少し待てば入力文字列と計算結果が返ってくると思います（このときフリーズする場合が多い）

#感想
- 文字列処理では、stringをbytesに戻さないとインデックス参照ができないのが面倒だった
- 状態変数が使えない場合、引数で常に文字列と入力位置を渡す必要があり面倒だった
- Remixで実行すると処理落ちでブラウザ更新しなければならなくなるのが面倒だった
- SafeMathを使ったり、StringのLibraryを使ったり、入力文字を状態変数に記録してview関数として作ったり、etc...　まだ遊べそうです