1024 バイトで Python インタープリターを構築 – Austin Z. Henley









オースティン・Z・ヘンリー

人のための道具を作っています



2026 年 9 月 6 日

1024 バイトで Python インタープリターを構築する

人間の感情のためにコードを書きます 手で 週末に。

私の最近の挑戦は? Python インタープリターを構築する 512 1024 バイトの優れた C コード。ああ、マクロの書き込みやライブラリの悪用はありません。

def buzz():
    for n in range(101):
        if n % 15 == 0:
            print("FizzBuzz")
        else:
            if n % 3 == 0:
                print("Fizz")
            else:
                if n % 5 == 0:
                    print("Buzz")
                else:
                    print(n)
buzz()

入り込めないかもしれない 全て インタプリタへの Python 言語。これはわずか 1024 バイトのコードです。では、この意志に対して何ができるでしょうか? ビュー パイソンのような?

この fizzbuzz プログラムは明らかに Python のように見えます。それは持っています 確かに括弧、括弧、および括弧なし もし ステートメントは私には Python のように見えます!もちろん、構文のサブセットだけでなく、いくつかの追加の制約を追加する必要があります。

最初の試みはダメでしたけどね。

最初の試行: 512 バイトでは不十分です!

私は再帰生成パーサーをたくさん書いてきましたが、これはどう違うのでしょうか? Python のサブセットは、私が実装した他の言語と似ているはずです。

私は思いつく限り最も単純なコードから始めました。 1+2

そこで、さらに複雑にしてみました。 x = 1 + 2 * 3

そして、さらに次のようなステートメントを追加しました。 x > y の場合: z = 3

よかった、電卓を作ってみた…この問題は私が考えていたものとは違いました!私ももう限界を超えていました。そこで、ズームインして要素のリストを作成しました。 ビュー Pythony ですが、同時に私のコーディング ゴルフ スキルが 512 バイトまでではないことにも気づきました。

1024バイトでやるべきでしょうか?最初にそれを実行してから減らします。

アナリスト

本物 CPython 実装では、Python ソースをレンダリングし、抽象構文ツリーに解析し、いくつかの解析と最適化を実行し、バイトコードを出力して、バイトコードを解釈します。

実際にはそんなことはありません。

状態は多数のグローバル変数に保存されます。生の Python コードを保持する固定長配列 (現在は 999) を使用します。変数と関数名はすべて配列に収まります。

char src[999];       /* Entire program without most spaces. */
int  vars[256];      /* Symbol table.                       */
int  pos;            /* Next character in src.              */
int  ch;             /* Current character in src.           */
int  line_start;     /* Where the current line starts.      */

式は再帰降下パーサーと同様に処理され、途中で実行されます。例えば:

int parse_sum(void) {
    int value = parse_term();
    while (ch == '+' || ch == '-') {
        if (ch == '+')
            value = value + parse_term();
        else
            value = value - parse_term();
    }
    return value;
}

それでも正しい。

エラーの種類はありません。作る たくさん コードの正確さに基づいて仮定を検証します。たとえば、キーワードのスペルがすべて正しいことを前提としています。

    if (ch == 'w' || ch == 'i' || ch == 'f') {
        /* ---- while / if / for ---- */
        int keyword = ch;
        int loop_var = 0;

        if (keyword == 'f') {             /* "for K in range(N):" */
            pos += 2;                     /* skip "or"             */
            loop_var = next();            /* the loop variable     */
            pos += 8;                     /* skip "inrange("       */
            vars[loop_var] = 0;
        } else if (keyword == 'w')
            pos += 4;                     /* skip "hile"           */
        else 
            pos += 1;                     /* skip "f" of "if"      */

また、文字の境界が正しいことを前提としており、空白の大部分が削除されます。文字列の文字のオフセットとスペースが保持されます。

単一文字の変数名に限定されているため、シンボル テーブルを直接検索できます。

    if (ch > 96) {
        value = vars[ch];
        next();
    }

制御フローの魔法

この関数は、ラインがなくなるまでコードのブロックを実行し続けます。これが発生すると、戻り値が返され、次の行を処理するのは呼び出し側の責任になります。したがって、C プログラムの呼び出しスタックを使用して再帰を処理します。

void run_block(int min_indent) {
    for (;;) {
        int indent = read_indent();

        if (ch == '\n')                       
            continue;

        if (indent < min_indent || ch == 0) {
            pos = line_start;
            return;
        }

しかし、指輪はどうでしょうか?

何もコンパイルされないため、ループは前後にジャンプして反復ごとにソースを再作成することによって機能します。両方 その間 そして のために ループは条件式の位置を追跡します。ボディが完了すると、その位置に戻り、分析を続けます。

これが関数の仕組みです。定義を解析するときに、シンボル テーブルはソース コード内の関数の位置を記憶します。次に、関数呼び出しを解析するときに、呼び出し元の場所が保存され、パーサーは関数の本体にジャンプして本体を実行し、終了時に呼び出し元の場所を復元します。

とても美しいので、リモート表現なしでも作業できます。翻訳者はまた、非常に控えめな姿勢を保っています。

減らす!

持っていない ゴルフのシンボル 変数名と空白文字が切り捨てられるのは明らかですが、大きなバイトを格納するにはどうすればよいでしょうか?

Stack Overflow という忘れ去られた Web サイトがあり、過去のコードウィザードが知識を共有していました。そこからたくさんのアイデアを学びました C 言語でゴルフをするためのヒント

スタック オーバーフローのコード ゴルフ スレッドのスクリーンショット。

ルールはあなたの想像の中にだけ存在するので、私は した 創造的であることが必要です。これらのヒントの一部は、特定の GNU C89 の「機能」に依存しています。これです いいえ 売春婦!これは伝統的なフィドルです。読み取り可能なバージョンからバイトを削除するために私が行ったことは次のとおりです。

  • 変数名と 1 文字の関数
  • コンパイラが libc をリンクすると仮定します。
  • 一時変数にはグローバルを使用する
  • グローバルはゼロで初期化されます
  • C89 では、変数宣言を暗黙的に int にすることができ、関数は int を返すものと想定されます。
  • 関数パラメータをコールスタックに保存される一時変数として使用する
  • リテラル文字の代わりに ASCII 値
  • 三項演算子とカンマ演算子
  • 論理演算ではなくビット演算

たとえば、 合計の解析 (無効) 先ほど紹介した関数はゴルフでプレイされました e(){for(z=t();c-43u<3;)y=44-c,z+=y*t();return z;}。 ASCII 値を使用して数バイトを削減します。

別の例は、行末に移動するヘルパー関数です。

void skip_to_eol(void) {
  if (ch != 0 && ch != '\n') {
    next();
    skip_to_eol();
  }
}

私はそれを置きました: Y(){c&&c-10&&Y(G());}。 0 をチェックし、減算して 10 を使用して改行をチェックします。 && の代わりに もし。あとは1バイト実行して保存 Y(G()); の代わりに G(); Y();。頭がいい! Stack Overflow の投稿に改めて感謝いたします。

やっぱりゴルフバージョンですね 1024 バイト!

最終的に読み取り可能なバージョンは 4800 バイトを超えます。もともとはもう少し機能があったのですが、必要に応じてカットしました。比較式は多くのバイトを消費し、真理値は比較式なしでも機能するため、次に比較式を使用します。 n% 15 の場合:

fizzbuzz のパフォーマンスだけを気にするのであれば、800 バイト未満にできると思います。他にもゴルフのコツがあるかもしれません。

ゴルフ コードのバイト長をチェックし、コンパイルして fizzbuzz を実行する端末のスクリーンショット。

ゴルフの栄光の源はここにあります。

char s[999];v[256],p,c,x,y,z,w,u;G(){return c=s[p++];}I(){for(u=p;G()==32;);return p-u;}Y(){c&&c-10&&Y(G());}f(){x=0;if(G()>96)x=v[c],G();for(;c-48u<10;G())x=x*10+c-48;return x;}t(g,h){for(g=f();c==42|c==37;)h=c,g=h-42?g%f():g*f();return g;}e(){for(z=t();c-43u<3;)y=44-c,z+=y*t();return z;}E(a,q){a=e();if(c-60u>2)return a;w=c-61;q=G()==61;p-=!q;x=e();return w?(a-x)*w>-q:a==x;}S(i){for(;I()>i|c==10;)Y();p=u;}Q(){for(G();G()-34;)putchar(c);G();}B(i,q,j,k,a,m,n){for(;;){j=I();if(c==10)continue;if(j96){k=c;while(G()>96);c==40?k-112?(G(),n=p,p=v[k],B(2),p=n,G()):(s[p]-34?printf("%d",E()):Q(),puts(""),G()):(v[k]=E());}Y();}}}main(q,m,h){for(h=m=q=0;~(c=getchar());){c=c-9?c:32;h^=c==34;s[q]=c;q+=c-32?1:!m|h;m=c>32|m&&c-10;}B(0);}

最終的に、次の機能を実装することができました。

  • 整数(単一文字)および数値変数
  • 変数の割り当て
  • + – * % を優先した算術演算 (単項 + – は式の先頭でのみ機能します)
  • との比較 < > <= >= == (ステートメントは 1 つだけ)
  • 全ての真実
  • もし そして 他の
  • その間 リングを含む 他の ブロック
  • 範囲(y)内のxの場合 リングを含む 他の ブロック
  • 引数のない関数定義
  • 関数呼び出し(再帰的呼び出しも含む)
  • ツールベースのブロック (スケールなし)
  • 印刷する リテラルまたは整数式文字列を使用
  • コメント

近い将来、これ以上象徴的なゴルフチャレンジをすることはないと思います。このプロセスは非常に面倒で、2 分前に何を変更したかを把握するために、進行中のバージョンと元のバージョンの間を行ったり来たりする必要がありました。両方のバージョンがアクティブです GitHub

今度はあなたの番です。あなたの Python は 1024 バイトでどのように見えるでしょうか?



Source link