2025-06-14

TurboWarpのアドオンを書いた

プログラミングをやる上で、文字列操作を避けて通ることはできない。

それは標準のブロックが貧弱1なScratchも例外ではないようで、Scratchの上位互換でおなじみのTurboWarpでは拡張機能として強力な文字列操作のブロックが追加されている。これでScratchでも自由自在に文字列操作し放題…というわけにはいかない。

ブロックのネストが鬼深くなってしまうのである。2

これはScratch/TurboWarp自体が持っている根本的な欠点であり、ちょっとしたブロック追加程度で解決できるものではない。ではどうすればいいのというと、より文字列操作に向いた言語を埋め込んでしまえばいいのである。

つくったもの

// Name: runBF
// ID: runBrainf**k
// Description: run Brainf**k
// License: WTFPL

((Scratch) => {
    "use strict";

    class RunBF {
        getInfo() {
            return {
                id: "runBrainfk",
                name: Scratch.translate("runBF"),
                blocks: [
                    {
                        opcode: "runBF",
                        blockType: Scratch.BlockType.REPORTER,
                        text: Scratch.translate(
                            "run [CODE] with input: [INPUT]",
                        ),
                        arguments: {
                            CODE: {
                                type: Scratch.ArgumentType.STRING,
                            },
                            INPUT: {
                                type: Scratch.ArgumentType.STRING,
                            },
                        },
                    },
                ],
            };
        }

        runBF(args: { CODE: string; INPUT: string }) {
            const tokens = parse(args.CODE);
            const f = compile(tokens);
            return f(args.INPUT)
        }
    }

    Scratch.extensions.register(new RunBF());
})(Scratch);

const TOKENS = ["+", "-", ">", "<", "[", "]", ".", ","] as const;
type Token = (typeof TOKENS)[number];

const parse = (s: string): [Token, number][] => {
    const s2 = [...s].filter((c) => TOKENS.includes(c as Token)) as Token[];
    const res: [Token, number][] = [];
    for (const [i, c] of s2.entries()) {
        if (s2[i - 1] !== c) res.push([c, 1]);
        else res[res.length - 1][1] += 1;
    }
    return res;
};

const MEM_SIZE = 65536;

const compile = (tokens: [Token, number][]) => {
    let code = `var input=(new TextEncoder()).encode(input);var output='';var read_ptr=0;var ptr=0;var mem=[];for(var i=0;i<${MEM_SIZE};i++){mem.push(0)};`;
    for (const [t, n] of tokens) {
        switch (t) {
            case "+":
                code += `mem[ptr]=(mem[ptr]+${n})%256;`;
                break;
            case "-":
                code += `mem[ptr]=(mem[ptr]-${n}+256)%256;`;
                break;
            case ">":
                code += `ptr=(ptr+${n})%${MEM_SIZE};`;
                break;
            case "<":
                code += `ptr=(ptr-${n}+${MEM_SIZE})%${MEM_SIZE};`;
                break;
            case "[":
                code += "while(mem[ptr]!==0){".repeat(n);
                break;
            case "]":
                code += "};".repeat(n);
                break;
            case ".":
                code += "output+=String.fromCharCode(mem[ptr]);".repeat(n);
                break;
            case ",":
                code += "mem[ptr]=input[read_ptr++]??0;".repeat(n);
                break;
        }
    }
    return new Function("input", `{${code}return output;}`);
};

Brainf**kを埋め込んだ。Brainf**kはチューリング完全で十分な表現力を持っているうえに、慣例では出力が文字列なのでまさに文字列操作に向いた言語と言える。入力もテキスト一つ分だけなのでセキュリティの観点から見ても完璧。

この実装はよく見るインタープリタ型と違い、JavaScriptにトランスパイルする形式なので動作が少し速い。letconstよりvarを使ったほうが速い3みたいな話を聞いたことがあったのでそれも試してみた。

さあ、みんなもBrainf**kで楽しい文字列操作ライフを送ろう!

脚注

  1. [もし端についたら、跳ね返る]みたいなブロックがあるわりに[縦に(100)%伸ばす]とかが無い謎 そこを補完してくれるTurboWarpはかなりありがたい存在

  2. 流石にこれは冗談だが、実際三項演算子なんかをネストさせていくとすぐ横長になっていって果てしなく見通しの悪いコードになる

  3. letconstは初期化前アクセスのチェックがある分わずかに遅いらしい

© 2025 banakna_79