然並卵系列:來寫個 Brainfuck 直譯器吧

創宇前端發表於2018-08-22

Lisp 是 write-only 語言?你忘記了這個圖靈完備的著名“亂碼”。


最近在 Codewars上做練習,某道題的內容是實現一個 brainFuck(簡稱BF語言) 直譯器。動手實踐的過程還是很有趣的,中間也遇到了各種各樣的問題,最終通過測試,程式碼也比較接近目前的 JS 高分 solution。這篇文章準備聊聊相關的一些知識和實現的細節。

“腦洞大開”的語言 —— BF 簡介

BrainFuck(後文以簡寫BF指代),單是名字就很容易讓人腦洞大開,有種不可描述的“哲學”韻味。所以如果你忍不住 google 一下相關圖片的話,你會可能搜到類似下面的圖片:

圖片來源:google 搜尋

畫面是不是已經很生動了?

BF 字面上的含義已經暗示了這是一種不太直觀和容易閱讀的語言,當然,在當下也不會是一種通用語言。她屬於 Esolang(全稱 Esoteric programming language,直譯:深奧的程式語言) 的範疇。

BF誕生於上世紀30年代,曾運用於早期的 PC(Amiga),想詳細瞭解的童鞋可以瀏覽 維基百科

BF 在當下有什麼應用場景呢?

我想,對一個吃瓜群眾來說,瞭解了它,對寫作 逼格腦力 的提升是很有用的。BF 具有極簡主義(搞設計的童鞋的不妨瞭解下下)和功能齊全(圖靈完全)的特點,旨在為使用者帶來困惑和挑戰,豐富勞動人民的業餘生活。

8 種運算子及其操作

BF 作為一種極簡的計算機語言,僅有8種運算子,分別為: <
>
+ - , . [ ],其功能對照如下表所示:

指令 含義
<
指標減一(指標左移)
>
指標加一(指標右移)
+ 指標指向的位元組的值加一(當前單元的數值+1)
- 指標指向的位元組的值減一(當前單元的數值-1)
, 輸入內容到指標指向的單元(輸入一個字元,將其ASCII碼儲存到當前指標所指單元)
. 將指標指向的儲存單元的內容作為字元輸出(將ASCII碼輸出為字元)
[ 如果指標指向的儲存單元為零,向後跳轉到對應的 ] 指令處
] 如果指標指向的儲存單元不為零,向前跳轉到對應的 [ 指令處

BF基於一個簡單的機器模型,除了八個指令,這個機器還包括:一個以位元組為單位、被初始化為零的陣列、一個指向該陣列的指標(初始時指向陣列的第一個位元組)、以及用於輸入輸出的兩個位元組流。

對 BF 比較有意思的比擬可以是這樣的:

  1. 如果把機器記憶體看成是一個無限長的“小火車”(類似於ArrayList的資料結構),每個車廂(儲存單元)裡面的貨物預設都是數字 0,列車上僅有一個列車員(資料指標);
  2. <
    >
    相當於列車員在車廂間進行移動,只有當列車員在某節車廂時,才能對車廂的貨物進行操作;
  3. +- 相當於列車員對當前所在車廂的貨物進行增減;
  4. , 相當於列車在裝貨,列車員將當前所在車廂的貨物替換為貨運站輸入的單批次貨物(一個字元的ASCII碼);
  5. . 會將當前車廂裡的貨物名稱(單個字元)出來;
  6. [] 相當於列車員在滿足條件的兩節車廂間來回移動;

這裡要注意的是,陣列的每個單元都是一個位元組大小;- 命令允許溢位,它可以用 255 個 + 命令來代替。例如,當某個儲存單元的值為 255 時,其執行指令 + 的結果為 0。類似的, 0 執行指令 - 的結果為 255.

與通用語言的類比

據此,BF的運算子與通用語言的類比如下(以C語言為例):

BrainFuck C
<
--ptr;
>
++ptr;
+ ++*ptr;
- --*ptr;
, *ptr = getchar();
. putchar(*ptr);
[ while (*ptr) {
]
}

BF 直譯器的 JS 函式實現

程式碼奉上:

function brainLuck(code, input) { 
// @1 const inputChars = input.split('');
// @2 const codes = code.split('');
// @3 let codeIdx = 0;
const arr = [];
// @4 let arrIdx = 0;
let outputStr = '';
// @5 while (codeIdx <
code.length) {
// @6 const ops = codes[codeIdx];
const handleLeftBracket = () =>
{
// @7 if (~~arr[arrIdx] === 0) {
let cnt = 1;
while (cnt) {
codeIdx++;
if (codes[codeIdx] === '[') {
cnt += 1;

} if (codes[codeIdx] === ']') {
cnt -= 1;

}
}
}
};
const handleRightBracket = () =>
{
// @8 if (~~arr[arrIdx] !== 0) {
let cnt = 1;
while (cnt) {
codeIdx--;
if (codes[codeIdx] === ']') {
cnt += 1;

} if (codes[codeIdx] === '[') {
cnt -= 1;

}
}
}
};
switch (ops) {
// @9 case '>
'
: arrIdx += 1;
break;
case '<
'
: arrIdx -= 1;
break;
case '+': arr[arrIdx] = (~~arr[arrIdx] + 1) % 256;
break;
case '-': arr[arrIdx] = (~~arr[arrIdx] || 256) - 1;
break;
case ',': const iptChar = inputChars.shift();
arr[arrIdx] = iptChar ? iptChar.charCodeAt(0) : arr[arrIdx];
break;
case '.': outputStr += String.fromCharCode(arr[arrIdx]);
break;
case '[': handleLeftBracket();
break;
case ']': handleRightBracket();
break;

} codeIdx++;
// @10
} return outputStr;
// @11
}複製程式碼

實現思路闡述(與程式碼中註釋的序號對應):

(1) 我們實現了一個函式 brainLuck 用以模擬 BF 語言的解釋執行,函式 brainLuck 的用例如下:

const code = ',+[-.,+]';
const input = 'Parksben' + String.fromCharCode(255);
const output = brainLuck(code, input);
console.log(output);
// ->
'Parksben'
複製程式碼

(2) 將輸入的字串切割為單個字元,暫存進陣列 inputChars;

(3) 將 BF 程式切割為單個操作符,方便遍歷每個指令,用 codeIdx 作為下標進行遍歷;

(4) 宣告一個陣列 arr 用以模擬機器記憶體,過程產生的數值儲存到此陣列中;

(5) 用字串 outputStr 儲存程式的輸出;

(6) 遍歷 BF 運算子,對不同指令進行相應的操作;

(7) 方法 handleLeftBracket,用以匹配到與當前 [ 對應的 ](通過操作下標 codeIdx);

(8) 方法 handleRightBracket,用以匹配到與當前 ] 對應的 [(通過操作下標 codeIdx);

(9) 用以處理不同指令的 switch 語句;

(10) codeIdx 加一,以向前遍歷 codes;

(11) 程式輸出;

延伸閱讀

Brainfuck: a Programming Language or a Joke?

丹尼爾·克里斯托法尼的一些 BF 例項

深奧的程式語言 – 維基百科


文 / Parksben

專注 2/3D 視覺化與前端開發

編 / 熒聲

本文已由作者授權釋出,版權屬於創宇前端。歡迎註明出處轉載本文。本文連結:knownsec-fed.com/2018-08-13-…

想要看到更多來自知道創宇開發一線的分享,請搜尋關注我們的微信公眾號:創宇前端(KnownsecFED)。

然並卵系列:來寫個 Brainfuck 直譯器吧

歡迎留言討論,我們會盡可能回覆。

感謝您的閱讀。

來源:https://juejin.im/post/5b7d14ef51882543025acdfd

相關文章