shunshock!

しゅんそくになりたい開発者です。

日曜大工で、インタープリタ作っている話

私は趣味でCLI職人をしているのですが、ある日気づきました。 「業務上、標準関数を自分でいじれるプログラミング言語、一つぐらいあったほうが便利じゃね?」

で、作りました。

let main: fn = (x: string): None {
    let message: string = "Hello, " + x;
    print(message);
    return none;
};

main("Shunsock");

print関数も開発段階なので、まだ型名が出ています。味があっていいですね。

shunsuke.tsuchiya in ~/Hobby/rast on function λ rast -f tests/data/function.rast
Str("Hello, Shunsock")

言語開発をしようとするとデバッグが面倒になってくるので、デバッグオプションを差し込んでいます。

shunsuke.tsuchiya in ~/Hobby/rast on function λ rast -f tests/data/function.rast -d
Scanned Tokens:
  Let
  Identifier("main")
  Colon
  Fn
  Equal
  LeftParen
  Identifier("x")
  Colon
  StringType
  RightParen
  Colon
  NoneType
  LeftBrace
  Let
  Identifier("message")
  Colon
  StringType
  Equal
  StringLiteral("Hello, World!")
  Plus
  Identifier("x")
  Semicolon
  Print
  LeftParen
  Identifier("message")
  RightParen
  Semicolon
  Return
  NoneLiteral
  Semicolon
  RightBrace
  Semicolon
  Identifier("main")
  LeftParen
  StringLiteral("shunsock")
  RightParen
  Semicolon
  Eof


AST is created:
  AST[0]:
    Statement(FunctionDeclaration(FunctionDeclarationNode { name: "main", params: [("x", "string")], return_type: "none", body: [Statement(VariableDeclaration(VariableDeclarationNode { name: "message", var_type: "string", value: BinaryOperation(BinaryOperationNode { left: Literal(LiteralNode { value: String("Hello, World!") }), operator: "+", right: Identifier(IdentifierNode { name: "x" }) }) })), Statement(Print(PrintNode { expression: Identifier(IdentifierNode { name: "message" }) })), Statement(Return(Literal(LiteralNode { value: None })))] }))
  AST[1]:
    Expression(FunctionCall(FunctionCallNode { name: "main", arguments: [Literal(LiteralNode { value: String("shunsock") })] }))


Str("Hello, World!shunsock")

clapは優秀なAPIを提供してくれるのでこれだけでパースができます。(自分のCLIでは実際には複数ファイルでこれを書いている、詳細は下の方みてくれ)

let cmd = Command::new("rast")
            .about("AST Based Language")
            .author("shunsock")
            .version("0.1.0")
            .arg(Arg::new("expression").required(false).short('e'))
            .arg(Arg::new("file").required(false).short('f'))
            .arg(
                Arg::new("debug")
                    .short('d')
                    .long("debug")
                    .action(ArgAction::SetTrue)
                    .help("Enable debug mode"),
            );

let matches = cmd.get_matches();

let debug_mode: bool = matches.get_flag("debug");

明後日に同じような話をする予定ですが、ディレクトリはこんな感じに切るのがおすすめです。

src
├── main.rs # 本体
├── rast_error.rs
├── receiver.rs # Clapのユーザー入力の実体を送ってくれるやつ
├── source_code_loader.rs # サービス
├── virtual_machine # サービス
│   ├── ast.rs
│   ├── evaluator
│   │   ├── evaluation_error.rs
│   │   ├── function_table.rs
│   │   ├── symbol_table.rs
│   │   └── value.rs
│   ├── evaluator.rs
│   ├── parser
│   │   └── parser_error.rs
│   ├── parser.rs
│   ├── scanner
│   │   └── scanner_error.rs
│   ├── scanner.rs
│   └── token.rs
└── virtual_machine.rs

5 directories, 16 files

ここはウェブ開発とかだとModel, Router, Controller, Service, Repositoryなどで一段階切っちゃうパターンが多いかもですね。(どれを使うかは人や組織によるだろう) 今回の場合、ソースコードをOnelinerで入力するパターンとプログラムから読み込ませるパターンにデバッグオプションが付くだけで最終的にはどれもvirtual_machineというプログラムに行き着くため、サービス直置きって感じです。

とはいえ、インタープリタをそのまま一つのファイルに書くと世界が崩壊するので、feature in featureしています。具体的にはScannerとParserとEvaluatorです。それぞれ500 line - 1000 line位はあるので、もっと分割した方が良い説が濃厚かなという気持ちです。次の実装するとしたら標準関数なのですが、Evaluatorが相当膨れているので、その前にリファクタリング作業に追われることになるでしょう。今見るとASTはvirtual_machine直置きで、tokenがscannerなの謎ですね。Parserでも呼ばれるので場所変えようかな。

公開は当分先です。まだ、開発中ってことで。

ではまた。