日曜大工で、インタープリタ作っている話
私は趣味で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でも呼ばれるので場所変えようかな。
公開は当分先です。まだ、開発中ってことで。
ではまた。