用 JavaScript 寫一個超小型編譯器
這是一個用JavaScript編寫的編譯器,雖然是一個很小很小的並沒有什麼卵用的編譯器,但可以向我們展示編譯器的很多東西。今天我把它翻譯了出來,供大家學習和討論。
/** * 今天讓我們來寫一個編譯器,一個超級無敵小的編譯器!它小到如果把所有註釋刪去的話,大概只剩 * 200行左右的程式碼。 * * 我們將會用它將 lisp 風格的函式呼叫轉換為 C 風格。 * * 如果你對這兩種風格不是很熟悉,下面是一個簡單的介紹。 * * 假設我們有兩個函式,`add` 和 `subtract`,那麼它們的寫法將會是下面這樣: * * LISP C * * 2 + 2 (add 2 2) add(2, 2) * 4 - 2 (subtract 4 2) subtract(4, 2) * 2 + (4 - 2) (add 2 (subtract 4 2)) add(2, subtract(4, 2)) * * 很簡單對吧? * * 這個轉換就是我們將要做的事情。雖然這並不包含 LISP 或者 C 的全部語法,但它足以向我們 * 展示現代編譯器很多要點。 * */ /** * 大多數編譯器可以分成三個階段:解析(Parsing),轉換(Transformation)以及程式碼 * 生成(Code Generation) * * 1. *解析*是將最初原始的程式碼轉換為一種更加抽象的表示(譯者注:即AST)。* * * 2. *轉換*將對這個抽象的表示做一些處理,讓它能做到編譯器期望 * 它做到的事情。 * * 3. *程式碼生成*接收處理之後的程式碼表示,然後把它轉換成新的程式碼。 */ /** * 解析(Parsing) * ------- * * 解析一般來說會分成兩個階段:詞法分析(Lexical Analysis)和語法分析(Syntactic Analysis)。 * * 1. *詞法分析*接收原始程式碼,然後把它分割成一些被稱為 Token 的東西,這個過程是在詞法分析 * 器(Tokenizer或者Lexer)中完成的。 * * Token 是一個陣列,由一些程式碼語句的碎片組成。它們可以是數字、標籤、標點符號、運算子, * 或者其它任何東西。 * * 2. *語法分析* 接收之前生成的 Token,把它們轉換成一種抽象的表示,這種抽象的表示描述了代 * 碼語句中的每一個片段以及它們之間的關係。這被稱為中間表示(intermediate representation) * 或抽象語法樹(Abstract Syntax Tree, 縮寫為AST) * * 抽象語法樹是一個巢狀程度很深的物件,用一種更容易處理的方式代表了程式碼本身,也能給我們 * 更多資訊。 * * 比如說對於下面這一行程式碼語句: * * (add 2 (subtract 4 2)) * * 它產生的 Token 看起來或許是這樣的: * * [ * { type: 'paren', value: '(' }, * { type: 'name', value: 'add' }, * { type: 'number', value: '2' }, * { type: 'paren', value: '(' }, * { type: 'name', value: 'subtract' }, * { type: 'number', value: '4' }, * { type: 'number', value: '2' }, * { type: 'paren', value: ')' }, * { type: 'paren', value: ')' } * ] * * 它的抽象語法樹(AST)看起來或許是這樣的: * * { * type: 'Program', * body: [{ * type: 'CallExpression', * name: 'add', * params: [{ * type: 'NumberLiteral', * value: '2' * }, { * type: 'CallExpression', * name: 'subtract', * params: [{ * type: 'NumberLiteral', * value: '4' * }, { * type: 'NumberLiteral', * value: '2' * }] * }] * }] * } */ /** * 轉換(Transformation) * -------------- * * 編譯器的下一步就是轉換。它只是把 AST 拿過來然後對它做一些修改。它可以在同種語言下操 * 作 AST,也可以把 AST 翻譯成全新的語言。 * * 下面我們來看看該如何轉換 AST。 * * 你或許注意到了我們的 AST 中有很多相似的元素,這些元素都有 type 屬性,它們被稱為 AST * 結點。這些結點含有若干屬性,可以用於描述 AST 的部分資訊。 * * 比如下面是一個“NumberLiteral”結點: * * { * type: 'NumberLiteral', * value: '2' * } * * 又比如下面是一個“CallExpression”結點: * * { * type: 'CallExpression', * name: 'subtract', * params: [...nested nodes go here...] * } * * 當轉換 AST 的時候我們可以新增、移動、替代這些結點,也可以根據現有的 AST 生成一個全新 * 的 AST * * 既然我們編譯器的目標是把輸入的程式碼轉換為一種新的語言,所以我們將會著重於產生一個針對 * 新語言的全新的 AST。 * * * 遍歷(Traversal) * --------- * * 為了能處理所有的結點,我們需要遍歷它們,使用的是深度優先遍歷。 * * { * type: 'Program', * body: [{ * type: 'CallExpression', * name: 'add', * params: [{ * type: 'NumberLiteral', * value: '2' * }, { * type: 'CallExpression', * name: 'subtract', * params: [{ * type: 'NumberLiteral', * value: '4' * }, { * type: 'NumberLiteral', * value: '2' * }] * }] * }] * } * * So for the above AST we would go: * 對於上面的 AST 的遍歷流程是這樣的: * * 1. Program - 從 AST 的頂部結點開始 * 2. CallExpression (add) - Program 的第一個子元素 * 3. NumberLiteral (2) - CallExpression (add) 的第一個子元素 * 4. CallExpression (subtract) - CallExpression (add) 的第二個子元素 * 5. NumberLiteral (4) - CallExpression (subtract) 的第一個子元素 * 6. NumberLiteral (4) - CallExpression (subtract) 的第二個子元素 * * 如果我們直接在 AST 內部操作,而不是產生一個新的 AST,那麼就要在這裡介紹所有種類的抽象, * 但是目前訪問(visiting)所有結點的方法已經足夠了。 * * 使用“訪問(visiting)”這個詞的是因為這是一種模式,代表在物件結構內對元素進行操作。 * * 訪問者(Visitors) * -------- * * 我們最基礎的想法是建立一個“訪問者(visitor)”物件,這個物件中包含一些方法,可以接收不 * 同的結點。 * * var visitor = { * NumberLiteral() {}, * CallExpression() {} * }; * * 當我們遍歷 AST 的時候,如果遇到了匹配 type 的結點,我們可以呼叫 visitor 中的方法。 * * 一般情況下為了讓這些方法可用性更好,我們會把父結點也作為引數傳入。 */ /** * 程式碼生成(Code Generation) * --------------- * * 編譯器的最後一個階段是程式碼生成,這個階段做的事情有時候會和轉換(transformation)重疊, * 但是程式碼生成最主要的部分還是根據 AST 來輸出程式碼。 * * 程式碼生成有幾種不同的工作方式,有些編譯器將會重用之前生成的 token,有些會建立獨立的程式碼 * 表示,以便於線性地輸出程式碼。但是接下來我們還是著重於使用之前生成好的 AST。 * * 我們的程式碼生成器需要知道如何“列印”AST 中所有型別的結點,然後它會遞迴地呼叫自身,直到所 * 有程式碼都被列印到一個很長的字串中。 * */ /** * 好了!這就是編譯器中所有的部分了。 * * 當然不是說所有的編譯器都像我說的這樣。不同的編譯器有不同的目的,所以也可能需要不同的步驟。 * * 但你現在應該對編譯器到底是個什麼東西有個大概的認識了。 * * 既然我全都解釋一遍了,你應該能寫一個屬於自己的編譯器了吧? * * 哈哈開個玩笑,接下來才是重點 : P * * 所以我們開始吧... */ /** * ============================================================================ * (/^▽^)/ * 詞法分析器(Tokenizer)! * ============================================================================ */ /** * 我們從第一個階段開始,即詞法分析,使用的是詞法分析器(Tokenizer)。 * * 我們只是接收程式碼組成的字串,然後把它們分割成 token 組成的陣列。 * * (add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...] */ // 我們從接收一個字串開始,首先設定兩個變數。 function tokenizer(input) { // `current`變數類似指標,用於記錄我們在程式碼字串中的位置。 var current = 0; // `tokens`陣列是我們放置 token 的地方 var tokens = []; // 首先我們建立一個 `while` 迴圈, `current` 變數會在迴圈中自增。 // // 我們這麼做的原因是,由於 token 陣列的長度是任意的,所以可能要在單個迴圈中多次 // 增加 `current` while (current < input.length) { // 我們在這裡儲存了 `input` 中的當前字元 var char = input[current]; // 要做的第一件事情就是檢查是不是右圓括號。這在之後將會用在 `CallExpressions` 中, // 但是現在我們關心的只是字元本身。 // // 檢查一下是不是一個左圓括號。 if (char === '(') { // 如果是,那麼我們 push 一個 type 為 `paren`,value 為左圓括號的物件。 tokens.push({ type: 'paren', value: '(' }); // 自增 `current` current++; // 結束本次迴圈,進入下一次迴圈 continue; } // 然後我們檢查是不是一個右圓括號。這裡做的時候和之前一樣:檢查右圓括號、加入新的 token、 // 自增 `current`,然後進入下一次迴圈。 if (char === ')') { tokens.push({ type: 'paren', value: ')' }); current++; continue; } // 繼續,我們現在檢查是不是空格。有趣的是,我們想要空格的本意是分隔字元,但這現在 // 對於我們儲存 token 來說不那麼重要。我們暫且擱置它。 // // 所以我們只是簡單地檢查是不是空格,如果是,那麼我們直接進入下一個迴圈。 var WHITESPACE = /\s/; if (WHITESPACE.test(char)) { current++; continue; } // 下一個 token 的型別是數字。它和之前的 token 不同,因為數字可以由多個數字字元組成, // 但是我們只能把它們識別為一個 token。 // // (add 123 456) // ^^^ ^^^ // Only two separate tokens // 這裡只有兩個 token // // 當我們遇到一個數字字元時,將會從這裡開始。 var NUMBERS = /[0-9]/; if (NUMBERS.test(char)) { // 建立一個 `value` 字串,用於 push 字元。 var value = ''; // 然後我們迴圈遍歷接下來的字元,直到我們遇到的字元不再是數字字元為止,把遇到的每 // 一個數字字元 push 進 `value` 中,然後自增 `current`。 while (NUMBERS.test(char)) { value += char; char = input[++current]; } // 然後我們把型別為 `number` 的 token 放入 `tokens` 陣列中。 tokens.push({ type: 'number', value: value }); // 進入下一次迴圈。 continue; } // 最後一種型別的 token 是 `name`。它由一系列的字母組成,這在我們的 lisp 語法中 // 代表了函式。 // // (add 2 4) // ^^^ // Name token // var LETTERS = /[a-z]/i; if (LETTERS.test(char)) { var value = ''; // 同樣,我們用一個迴圈遍歷所有的字母,把它們存入 value 中。 while (LETTERS.test(char)) { value += char; char = input[++current]; } // 然後新增一個型別為 `name` 的 token,然後進入下一次迴圈。 tokens.push({ type: 'name', value: value }); continue; } // 最後如果我們沒有匹配上任何型別的 token,那麼我們丟擲一個錯誤。 throw new TypeError('I dont know what this character is: ' + char); } // 詞法分析器的最後我們返回 tokens 陣列。 return tokens; } /** * ============================================================================ * ヽ/❀o ل͜ o\ノ * 語法分析器(Parser)!!! * ============================================================================ */ /** * 語法分析器接受 token 陣列,然後把它轉化為 AST * * [{ type: 'paren', value: '(' }, ...] => { type: 'Program', body: [...] } */ // 現在我們定義 parser 函式,接受 `tokens` 陣列 function parser(tokens) { // 我們再次宣告一個 `current` 變數作為指標。 var current = 0; // 但是這次我們使用遞迴而不是 `while` 迴圈,所以我們定義一個 `walk` 函式。 function walk() { // walk函式裡,我們從當前token開始 var token = tokens[current]; // 對於不同型別的結點,對應的處理方法也不同,我們從 `number` 型別的 token 開始。 // 檢查是不是 `number` 型別 if (token.type === 'number') { // 如果是,`current` 自增。 current++; // 然後我們會返回一個新的 AST 結點 `NumberLiteral`,並且把它的值設為 token 的值。 return { type: 'NumberLiteral', value: token.value }; } // 接下來我們檢查是不是 CallExpressions 型別,我們從左圓括號開始。 if ( token.type === 'paren' && token.value === '(' ) { // 我們會自增 `current` 來跳過這個括號,因為括號在 AST 中是不重要的。 token = tokens[++current]; // 我們建立一個型別為 `CallExpression` 的根節點,然後把它的 name 屬性設定為當前 // token 的值,因為緊跟在左圓括號後面的 token 一定是呼叫的函式的名字。 var node = { type: 'CallExpression', name: token.value, params: [] }; // 我們再次自增 `current` 變數,跳過當前的 token token = tokens[++current]; // 現在我們迴圈遍歷接下來的每一個 token,直到我們遇到右圓括號,這些 token 將會 // 是 `CallExpression` 的 `params`(引數) // // 這也是遞迴開始的地方,我們採用遞迴的方式來解決問題,而不是去嘗試解析一個可能有無限 // 層巢狀的結點。 // // 為了更好地解釋,我們來看看我們的 Lisp 程式碼。你會注意到 `add` 函式的引數有兩個, // 一個是數字,另一個是一個巢狀的 `CallExpression`,這個 `CallExpression` 中 // 包含了它自己的引數(兩個數字) // // (add 2 (subtract 4 2)) // // 你也會注意到我們的 token 陣列中有多個右圓括號。 // // [ // { type: 'paren', value: '(' }, // { type: 'name', value: 'add' }, // { type: 'number', value: '2' }, // { type: 'paren', value: '(' }, // { type: 'name', value: 'subtract' }, // { type: 'number', value: '4' }, // { type: 'number', value: '2' }, // { type: 'paren', value: ')' }, <<< 右圓括號 // { type: 'paren', value: ')' } <<< 右圓括號 // ] // // 遇到巢狀的 `CallExpressions` 時,我們將會依賴巢狀的 `walk` 函式來 // 增加 `current` 變數 // // 所以我們建立一個 `while` 迴圈,直到遇到型別為 `'paren'`,值為右圓括號的 token。 while ( (token.type !== 'paren') || (token.type === 'paren' && token.value !== ')') ) { // 我們呼叫 `walk` 函式,它將會返回一個結點,然後我們把這個節點 // 放入 `node.params` 中。 node.params.push(walk()); token = tokens[current]; } // 我們最後一次增加 `current`,跳過右圓括號。 current++; // 返回結點。 return node; } // 同樣,如果我們遇到了一個型別未知的結點,就丟擲一個錯誤。 throw new TypeError(token.type); } // 現在,我們建立 AST,根結點是一個型別為 `Program` 的結點。 var ast = { type: 'Program', body: [] }; // 現在我們開始 `walk` 函式,把結點放入 `ast.body` 中。 // // 之所以在一個迴圈中處理,是因為我們的程式可能在 `CallExpressions` 後面包含連續的兩個 // 引數,而不是巢狀的。 // // (add 2 2) // (subtract 4 2) // while (current < tokens.length) { ast.body.push(walk()); } // 最後我們的語法分析器返回 AST return ast; } /** * ============================================================================ * ⌒(❀>◞౪◟<❀)⌒ * 遍歷器!!! * ============================================================================ */ /** * 現在我們有了 AST,我們需要一個 visitor 去遍歷所有的結點。當遇到某個型別的結點時,我們 * 需要呼叫 visitor 中對應型別的處理函式。 * * traverse(ast, { * Program(node, parent) { * // ... * }, * * CallExpression(node, parent) { * // ... * }, * * NumberLiteral(node, parent) { * // ... * } * }); */ // 所以我們定義一個遍歷器,它有兩個引數,AST 和 vistor。在它的裡面我們又定義了兩個函式... function traverser(ast, visitor) { // `traverseArray` 函式允許我們對陣列中的每一個元素呼叫 `traverseNode` 函式。 function traverseArray(array, parent) { array.forEach(function(child) { traverseNode(child, parent); }); } // `traverseNode` 函式接受一個 `node` 和它的父結點 `parent` 作為引數,這個結點會被 // 傳入到 visitor 中相應的處理函式那裡。 function traverseNode(node, parent) { // 首先我們看看 visitor 中有沒有對應 `type` 的處理函式。 var method = visitor[node.type]; // 如果有,那麼我們把 `node` 和 `parent` 都傳入其中。 if (method) { method(node, parent); } // 下面我們對每一個不同型別的結點分開處理。 switch (node.type) { // 我們從頂層的 `Program` 開始,Program 結點中有一個 body 屬性,它是一個由若干 // 個結點組成的陣列,所以我們對這個陣列呼叫 `traverseArray`。 // // (記住 `traverseArray` 會呼叫 `traverseNode`,所以我們會遞迴地遍歷這棵樹。) case 'Program': traverseArray(node.body, node); break; // 下面我們對 `CallExpressions` 做同樣的事情,遍歷它的 `params`。 case 'CallExpression': traverseArray(node.params, node); break; // 如果是 `NumberLiterals`,那麼就沒有任何子結點了,所以我們直接 break case 'NumberLiteral': break; // 同樣,如果我們不能識別當前的結點,那麼就丟擲一個錯誤。 default: throw new TypeError(node.type); } } // 最後我們對 AST 呼叫 `traverseNode`,開始遍歷。注意 AST 並沒有父結點。 traverseNode(ast, null); } /** * ============================================================================ * ⁽(◍˃̵͈̑ᴗ˂̵͈̑)⁽ * 轉換器!!! * ============================================================================ */ /** * 下面是轉換器。轉換器接收我們在之前構建好的 AST,然後把它和 visitor 傳遞進入我們的遍歷 * 器中 ,最後得到一個新的 AST。 * * ---------------------------------------------------------------------------- * 原始的 AST | 轉換後的 AST * ---------------------------------------------------------------------------- * { | { * type: 'Program', | type: 'Program', * body: [{ | body: [{ * type: 'CallExpression', | type: 'ExpressionStatement', * name: 'add', | expression: { * params: [{ | type: 'CallExpression', * type: 'NumberLiteral', | callee: { * value: '2' | type: 'Identifier', * }, { | name: 'add' * type: 'CallExpression', | }, * name: 'subtract', | arguments: [{ * params: [{ | type: 'NumberLiteral', * type: 'NumberLiteral', | value: '2' * value: '4' | }, { * }, { | type: 'CallExpression', * type: 'NumberLiteral', | callee: { * value: '2' | type: 'Identifier', * }] | name: 'subtract' * }] | }, * }] | arguments: [{ * } | type: 'NumberLiteral', * | value: '4' * ---------------------------------- | }, { * | type: 'NumberLiteral', * | value: '2' * | }] * (那一邊比較長/w\) | }] * | } * | }] * | } * ---------------------------------------------------------------------------- */ // 定義我們的轉換器函式,接收 AST 作為引數 function transformer(ast) { // 建立 `newAST`,它與我們之前的 AST 類似,有一個型別為 Program 的根節點。 var newAst = { type: 'Program', body: [] }; // 下面的程式碼會有些奇技淫巧,我們在父結點上使用一個屬性 `context`(上下文),這樣我們就 // 可以把結點放入他們父結點的 context 中。當然可能會有更好的做法,但是為了簡單我們姑且 // 這麼做吧。 // // 注意 context 是一個*引用*,從舊的 AST 到新的 AST。 ast._context = newAst.body; // 我們把 AST 和 visitor 函式傳入遍歷器 traverser(ast, { // 第一個 visitor 方法接收 `NumberLiterals`。 NumberLiteral: function(node, parent) { // 我們建立一個新結點,名字叫 `NumberLiteral`,並把它放入父結點的 context 中。 parent._context.push({ type: 'NumberLiteral', value: node.value }); }, // 下一個,`CallExpressions`。 CallExpression: function(node, parent) { // 我們建立一個 `CallExpression` 結點,裡面有一個巢狀的 `Identifier`。 var expression = { type: 'CallExpression', callee: { type: 'Identifier', name: node.name }, arguments: [] }; // 下面我們在原來的 `CallExpression` 結點上定義一個新的 context,它是 expression // 中 arguments 這個陣列的引用,我們可以向其中放入引數。 node._context = expression.arguments; // 然後來看看父結點是不是一個 `CallExpression`,如果不是... if (parent.type !== 'CallExpression') { // 我們把 `CallExpression` 結點包在一個 `ExpressionStatement` 中,這麼做是因為 // 單獨存在(原文為top level)的 `CallExpressions` 在 JavaScript 中也可以被當做 // 是宣告語句。 // // 譯者注:比如 `var a = foo()` 與 `foo()`,後者既可以當作表示式給某個變數賦值,也 // 可以作為一個獨立的語句存在。 expression = { type: 'ExpressionStatement', expression: expression }; } // 最後我們把 `CallExpression`(可能是被包起來的) 放入父結點的 context 中。 parent._context.push(expression); } }); // 最後返回建立好的新 AST。 return newAst; } /** * ============================================================================ * ヾ(〃^∇^)ノ♪ * 程式碼生成器!!!! * ============================================================================ */ /** * 現在只剩最後一步啦:程式碼生成器。 * * 我們的程式碼生成器會遞迴地呼叫它自己,把 AST 中的每個結點列印到一個很大的字串中。 */ function codeGenerator(node) { // 對於不同 `type` 的結點分開處理。 switch (node.type) { // 如果是 `Program` 結點,那麼我們會遍歷它的 `body` 屬性中的每一個結點,並且遞迴地 // 對這些結點再次呼叫 codeGenerator,再把結果列印進入新的一行中。 case 'Program': return node.body.map(codeGenerator) .join('\n'); // 對於 `ExpressionStatements`,我們對它的 expression 屬性遞迴呼叫,同時加入一個 // 分號。 case 'ExpressionStatement': return ( codeGenerator(node.expression) + ';' // << (...因為我們喜歡用*正確*的方式寫程式碼) ); // 對於 `CallExpressions`,我們會列印出 `callee`,接著是一個左圓括號,然後對 // arguments 遞迴呼叫 codeGenerator,並且在它們之間加一個逗號,最後加上右圓括號。 case 'CallExpression': return ( codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator) .join(', ') + ')' ); // 對於 `Identifiers` 我們只是返回 `node` 的 name。 case 'Identifier': return node.name; // 對於 `NumberLiterals` 我們只是返回 `node` 的 value case 'NumberLiteral': return node.value; // 如果我們不能識別這個結點,那麼丟擲一個錯誤。 default: throw new TypeError(node.type); } } /** * ============================================================================ * (۶* ‘ヮ’)۶” * !!!!!!!!!!!!編譯器!!!!!!!!!!! * ============================================================================ */ /** * 最後!我們建立 `compiler` 函式,它只是把上面說到的那些函式連線到一起。 * * 1. input => tokenizer => tokens * 2. tokens => parser => ast * 3. ast => transformer => newAst * 4. newAst => generator => output */ function compiler(input) { var tokens = tokenizer(input); var ast = parser(tokens); var newAst = transformer(ast); var output = codeGenerator(newAst); // 然後返回輸出! return output; } /** * ============================================================================ * (๑˃̵ᴗ˂̵)و * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!你做到了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! * ============================================================================ */ // 現在匯出所有介面... module.exports = { tokenizer: tokenizer, parser: parser, transformer: transformer, codeGenerator: codeGenerator, compiler: compiler };
相關文章
- javascript編寫一個簡單的編譯器JavaScript編譯
- 前端與編譯原理——用 JS 寫一個 JS 直譯器前端編譯原理JS
- 前端與編譯原理——用JS寫一個JS直譯器前端編譯原理JS
- 用 golang 寫一個語言(編譯器,虛擬機器)Golang編譯虛擬機
- Google提供了一個Javascript編譯器GoJavaScript編譯
- javascript編寫一個簡單的編譯器(理解抽象語法樹AST)JavaScript編譯抽象語法樹AST
- 【譯】用JavaScript寫一個區塊鏈JavaScript區塊鏈
- 編譯原理實戰入門:用 JavaScript 寫一個簡單的四則運算編譯器(修訂版)編譯原理JavaScript
- 編寫一個非常簡單的 JavaScript 編輯器JavaScript
- [譯] 用javascript實現一門程式語言-寫一個解析器JavaScript
- JavaScript 編寫的迷你 Lisp 直譯器JavaScriptLisp
- 用java寫一個lisp 直譯器JavaLisp
- [譯] 如何編寫全棧 JavaScript 應用全棧JavaScript
- 實現一個簡單的 JavaScript 編譯器JavaScript編譯
- 如何使用Python編寫一個Lisp直譯器PythonLisp
- [譯]用Golang編寫一個簡易聊天室Golang
- 第一個C語言編譯器是怎樣編寫的?C語言編譯
- 第一個 C 語言編譯器是怎樣編寫的?編譯
- [譯] 為什麼我用 JavaScript 來編寫 CSSJavaScriptCSS
- [譯] 編寫函式式的 JavaScript 實用指南函式JavaScript
- JavaScript編譯器BabelJavaScript編譯Babel
- 「 giao-js 」用js寫一個js直譯器JS
- 用 JavaScript 編寫 MPEG1 解碼器JavaScript
- 用C寫一個web伺服器(三) Linux下用GCC進行專案編譯Web伺服器LinuxGC編譯
- 用Swing寫了一個Class編輯器:ClassEditor
- 用 Haskell 編寫 CEK 風格的直譯器Haskell
- Vue寫一個Markdown編輯器Vue
- [譯] 使用 Vue 編寫一個長按指令Vue
- 【譯】編寫更好JavaScript條件語句的5個技巧JavaScript
- [譯] 教程 — 用 C 寫一個 Shell
- [譯] 用 Rust 寫一個微服務Rust微服務
- 編寫一個簡單的JavaScript模板引擎JavaScript
- 用 JavaScript 寫一個卡片小遊戲JavaScript遊戲
- 用 JavaScript 寫一個區塊鏈JavaScript區塊鏈
- 使用C編譯器編寫shellcode編譯
- NDK clang編譯器的一個bug編譯
- [譯] 如何在瀏覽器中編寫一款藍芽應用瀏覽器藍芽
- [譯] Go 終極指南:編寫一個 Go 工具Go