Ajisai コンパイラ育成日誌 第2日

前回は、以下のC言語のソースコードをファイルに書き出して、それをコンパイルするだけのプログラムを書きました:

// C 言語のソースコード
const cSource = `#include <stdio.h>

int main() {
  printf("%d\\n", 42);
  return 0;
}
`;

まだ Ajisai 言語がどんな言語仕様なのかといった議論は何もしておらず、無から定型の実行ファイルが生成されるだけの、全く自由のないコンパイラ(?)です。ここで、とりあえず整数値の 42 の部分だけを抜き出して、Ajisai 言語のソースコードと言い張ってみましょう:

// Ajisai 言語のソースコード
const ajisaiSource = "42";

// C 言語のソースコード
const cSource = `#include <stdio.h>

int main() {
  printf("%d\\n", ${ajisaiSource});
  return 0;
}
`;

ちょっと進歩が見られましたが、コンパイラのユーザーがわざわざコンパイラのソースコードを開いて ajisaiSource の値を変更しないと、好きな整数値に変更できませんね。ajisaiSource に与える文字列を、外部のファイルから取得できるように変更してみます:

// Ajisai 言語のソースコードをファイルから読み込む
const ajisaiSource = Deno.readTextFileSync(sourceFilePath);

しかし、sourceFilePath はどうやって取得しましょう? Deno.args から直接コマンドライン引数を取得しても良いのですが、引数の解析が面倒そうです……。良さげな argument parser ライブラリの cliffy を見つけたので使ってみることにしました:

import { Command } from "jsr:@cliffy/command@^1.0.0-rc.8";

// build コマンドのオプションはなし
type Options = Record<string, never>;
// build コマンドの引数: <sourceFilePath:string>
type Arguments = [string];

// サブコマンド build の処理
function buildMain(_options: Options, ...args: Arguments) {
  const [sourceFilePath] = args;
  // ...
}

const build = new Command()
  .arguments("<source:string>")
  .description("Create executable from source files.")
  .action(buildMain);

await new Command()
  .name("ajisai")
  .version("0.0.1")
  .description("Ajisai compiler")
  .command("build", build)
  .parse(Deno.args);

そして、これまで書いてきた処理は buildMain 関数の中に移します。それから、その処理の前段階で sourceFilePath の指すファイルからテキストを読んで ajisaiSource 変数の値として設定します:

function buildMain(_options: Options, ...args: Arguments) {
  // sourceFilePath の存在確認
  const [sourceFilePath] = args;
  try {
    const sourceFileStat = Deno.statSync(sourceFilePath);
    if (!sourceFileStat.isFile) {
      console.error(`"${sourceFilePath}" found, but not a file`);
      Deno.exit(1);
    }
  } catch {
    console.error(`"${sourceFilePath}" not found`);
    Deno.exit(1);
  }
  // Ajisai 言語のソースコードをファイルから読み込む
  const ajisaiSource = Deno.readTextFileSync(sourceFilePath);

  // ...
}

使ってみましょう。試しにソースファイルを examples/answer.ajs という名前で作って、そこからコンパイルしてみます:

$ deno -A ajisai.ts --help

Usage:   ajisai
Version: 0.0.1

Description:

  Ajisai compiler

Options:

  -h, --help     - Show this help.
  -V, --version  - Show the version number for this program.

Commands:

  build  <source>  - Create executable from source files.

$ cat examples/answer.ajs
42
$ deno -A ajisai.ts build examples/answer.ajs
$ ./ajisai-out/main
42

うまくいってそうですね。

ところで、現状は Ajisai のソースコードをそのままC言語のソースコードの中に埋め込んでいるだけです。ということは、Ajisai の整数演算の仕様がC言語と同じだと仮定すると、ソースコードとして算術演算を書いてみてもうまくいきそうです。やってみましょう:

$ cat examples/arith.ajs
(10 + 11) * 2
$ deno -A ajisai.ts build examples/arith.ajs
$ ./ajisai-out/main
42

うまくいきました。しかも、構文エラーもCコンパイラが報告してくれます(当たり前ですが……):

$ cat examples/invalid.ajs
(10 + 11) *
$ deno -A ajisai.ts build examples/invalid.ajs
ajisai-out/main.c:5:1: error: expected expression
    5 | );
      | ^
1 error generated.

今はこれでうまくいっていますが、今後は間違いなく言語仕様がC言語とは異なってくるので、構文解析や意味解析を自前でやっていく必要がありますね。次回からはそれに取り組んでいきたいと思います。

(今回のコード: day001-010/day002