手寫 compiler

看 babel doc 的時(shí)候,首頁看到了一個(gè)鏈接,說是能從 high level 解釋 babel 是怎么工作的 !!!

以下是babel官網(wǎng)提供的鏈接中的大致內(nèi)容,有興趣的戳這里看原文

簡單說明

編譯 是從 源代碼(通常為高級語言) 到能直接被計(jì)算機(jī)編譯器或虛擬機(jī)執(zhí)行的 目標(biāo)代碼(通常為低級語言或機(jī)器語言) 的翻譯過程.

如何用 JS 寫一個(gè)超級小巧 (200行代碼預(yù)定) 的編譯器.

我們即將 (通過這個(gè)編譯器) 把一些看著像 lisp 的代碼, 編譯成類 C 代碼.
其實(shí)和把 ES6 轉(zhuǎn)為 ES5, JSX/TS 轉(zhuǎn)為 JS 一個(gè)套路.

具體的例子就是這個(gè)樣子

                    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 語法. 但足夠展示現(xiàn)代編譯器的主要部分了.

背景

大多數(shù)編譯器分為三個(gè)主要階段:解析皆看,轉(zhuǎn)換,代碼生成.

  1. 解析 : 將 原始代碼 轉(zhuǎn)換為 更抽象的表示背零。
  2. 轉(zhuǎn)換 : 將這個(gè) 抽象的表示 轉(zhuǎn)化為編譯器想要的東西.
  3. 代碼生成 : 將轉(zhuǎn)化后的表示 轉(zhuǎn)化為新的代碼.

解析

解析通常分為兩個(gè)階段:詞法分析 和 句法分析

1 詞法分析 (Lexical Analysis)
獲取 原始代碼 并通過 標(biāo)記化器tokenizer(詞法分析器lexer) 將其 拆分為 tokens

tokens 是一組非常小的 object腰吟,描述語法中的一個(gè)獨(dú)立的部分。它們可能是數(shù)字徙瓶,標(biāo)簽毛雇,標(biāo)點(diǎn)符號嫉称,運(yùn)算符, 等等等等。

2 語法分析 (Syntactic Analysis)
獲取 token 并將其重新格式化為 描述語法的各個(gè)部分及其關(guān)系 的一種表現(xiàn)形式灵疮。這種表現(xiàn)形式稱為 中間語言(intermediate representation) , 也叫 抽象語法樹织阅。

比如:

(add 2 (subtract 4 2))

這其中的 tokens 大概會(huì)是:

[
    { 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: ')'        },
]

抽象語法樹 Abstract Syntax Tree (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',
            }]
        }]
    }]
}

這樣看起不爽, 其實(shí) body 中的內(nèi)容就是

        add
       /   \
     2    substract
         /      \
        4        2

轉(zhuǎn)換

將 AST 轉(zhuǎn)化為同樣的語言, 或者其他完全不同的語言.

看看怎么轉(zhuǎn)化 AST 吧.

你可能注意到了, 我們的 AST 中有一些元素, 看起來都差不多.
都有一個(gè) type 屬性. 這些我們把它叫做 AST 節(jié)點(diǎn).
這些節(jié)點(diǎn)定義了自身的屬性.

比如說這是一個(gè)數(shù)字:

{
    type: 'NumberLiteral',
    value: '2',
}

這是一個(gè)調(diào)用表達(dá)式:

{
    type: 'CallExpression',
    name: 'add',
    params: [ ...一些內(nèi)嵌節(jié)點(diǎn)... ]
}

轉(zhuǎn)換就是對 AST 節(jié)點(diǎn)的操作, 我們增加,刪除,替換里面的屬性.
也可以增加新的節(jié)點(diǎn), 刪除節(jié)點(diǎn).
我們也可以把這個(gè) AST 扔在這不操作, 而是基于它建一個(gè)新的出來.

因?yàn)槲覀冞@里, 要把代碼轉(zhuǎn)換為另一種語言(LIST to C), 我們后面就建一個(gè)新的 AST 好了.

遍歷

為了瀏覽節(jié)點(diǎn),我們需要能夠遍歷他們震捣。
此遍歷過程將使用 深度優(yōu)先 來訪問 AST 中的每個(gè)節(jié)點(diǎn).

對于這個(gè) AST.

        Program
          |
        add
       /   \
     2    substract
         /      \
        4        2

我們遍歷的順序是:

  1. Program
  2. CallExpression (add)
  3. NumberLiteral (2)
  4. CallExpression (substract)
  5. NumberLiteral (4)
  6. NumberLiteral (2)

如果我們要直接操作這棵樹的話, 介紹一下不同的遍歷方式還是有用的,
然而我們只是建一顆新的, 所以深度優(yōu)先遍歷足夠了.

訪客 vistor

基本思路是: 我們創(chuàng)建一個(gè)訪客 vistor 對象, 它有不同方法能接受不同類型 (type) 的節(jié)點(diǎn).

var visitor = {
    NumberLiteral() {},
    CallExpression() {},
};

遍歷過程中, 當(dāng)我們進(jìn)入 (enter) AST 中每一個(gè)節(jié)點(diǎn)的時(shí)候, 我們都會(huì)根據(jù)節(jié)點(diǎn) type 調(diào)用這個(gè) vistor 的對應(yīng)方法.

為了弄清楚類型, 這個(gè)函數(shù)需要接收當(dāng)前節(jié)點(diǎn) node
為了遍歷, 這個(gè)函數(shù)需要知道它的父節(jié)點(diǎn) parent

var visitor = {
    NumberLiteral(node, parent) {},
    CallExpression(node, parent) {},
};

不過, 我們可能還需要考慮一下 "退出 exit"
看一下樹:

- Program
    - CallExpression
        - NumberLiteral
        - CallExpression
            - NumberLiteral
            - NumberLiteral

當(dāng)我們遍歷時(shí), 可能走到?jīng)]有子節(jié)點(diǎn)的分支, 我們就該 exit 了,
所以向下遍歷我們叫做 "enter", 往回退我們叫做 "exit".

-> Program (enter)
    -> CallExpression (enter)
        -> Number Literal (enter)
        <- Number Literal (exit)
        -> Call Expression (enter)
            -> Number Literal (enter)
            <- Number Literal (exit)
            -> Number Literal (enter)
            <- Number Literal (exit)
        <- CallExpression (exit)
    <- CallExpression (exit)
<- Program (exit)

為了支持 enter 和 exit, 最終我們的 vistor 差不多是這樣:

var visitor = {
    NumberLiteral: {
        enter(node, parent) {},
        exit(node, parent) {},
    }
};

代碼生成

最后一步啦.

有一些 compiler 可能會(huì)把 transform 和這一步疊加在一起,
但是 代碼生成(code generation) 這一步的主要目的是, 把我們的AST轉(zhuǎn)為代碼字符串(string-ify)蒲稳。

代碼生成有很多種做法, 大多數(shù)就是用我們剛才生成的 AST 來做的.

我們的 code generator 應(yīng)該知道怎么打印出這些不同類型的 node (遞歸 nodes 是常見操作)

ta-da!

完畢, 這就是編譯器的所有不同部分.

并不是說每個(gè)編譯器看起來都像我在這里描述的那樣.編譯器有許多不同的用途,它們可能需要更多的步驟.
你現(xiàn)在應(yīng)該大概知道它是怎么運(yùn)作的.

代碼時(shí)間

點(diǎn)這里 獲取代碼

(/ ^ ▽ ^ )/ tokenizer

我們將從解析第一階段,詞法分析 tokenizer (token 化)開始.
也就是把代碼字符串, 轉(zhuǎn)化為一組 token

(add 2 (subtract 4 2))   =>   [{ type: 'paren', value: '(' }, ...]
function tokenizer(input) {
    // current 標(biāo)記我們現(xiàn)在在 code 的哪個(gè)位置, 其實(shí)就是 index
    let current = 0;
    let tokens = [];
    
    while (current < input.length) {
        let char = input[current];
        
        // 左右括號
        if (char === '(') {
            tokens.push({
                type: 'paren',
                value: '(',
            });
            current++;
            continue;
        }
        if (char === ')') {
            tokens.push({
                type: 'paren',
                value: ')',
            });
            current++;
            continue;
        }
        
        // 空格
        let WHITESPACE = /\s/;
        if (WHITESPACE.test(char)) {
            current++;
            continue;
        }
        
        // 數(shù)字
        let NUMBERS = /[0-9]/;
        if (NUMBERS.test(char)) {
            let value = '';
            while (NUMBERS.test(char)) {
                value += char;
                char = input[++current];
            }
            tokens.push({ type: 'number', value });s
            continue;
        }
        
        // 也處理一下 string 字符串
        if (char === '"') {
            let value = '';
            char = input[++current];
            while (char !== '"') {
                value += char;
                char = input[++current];
            }
            char = input[++current];
            tokens.push({ type: 'string', value });
            continue;
        }
        
        // 處理函數(shù)名
        let LETTERS = /[a-z]/i;
        if (LETTERS.test(char)) {
            let value = '';
            while (LETTERS.test(char)) {
                value += char;
                char = input[++current];
            }
            tokens.push({ type: 'name', value });
            continue;
        }
        
        // 識別不出來
        throw new TypeError('I dont know what this character is: ' + char);
    }
}

ヽ/?o ?? o\? parser

接收一個(gè) token 數(shù)組, 返回一個(gè) AST

 [{ type: 'paren', value: '(' }, ...]   =>   { type: 'Program', body: [...] }
// 接收一個(gè) token 數(shù)組
function parser(tokens) {
    let current = 0;
    
    // 遞歸 recursion
    function walk() {
        let token = tokens[current];
        
        if (token.type === 'number') {
            current++;
            // 數(shù)字節(jié)點(diǎn), 直接返回
            return {
                type: 'NumberLiteral',
                value: token.value,
            };
        }
        
        if (token.type === 'string') {
            current++;
            // 字符串節(jié)點(diǎn), 直接返回
            return {
                type: 'StringLiteral',
                value: token.value,
            };
        }
        
        // 遇到一個(gè)左括號, 調(diào)用表達(dá)式
        if (
            token.type === 'paren' &&
            token.value === '('
        ) {
            // 創(chuàng)建一個(gè)調(diào)用節(jié)點(diǎn)
            token = tokens[++current];
            let node = {
                type: 'CallExpression',
                name: token.value,
                params: [],
            };
            
            token = tokens[++current];
            while (
                (token.type !== 'paren') ||
                (token.type === 'paren' && token.value !== ')')
            ) {
                // 遇到右括號之前, 在這個(gè)節(jié)點(diǎn)下遍歷, 都放入調(diào)用參數(shù)中.
                node.params.push(walk());
                token = tokens[current];  
            }
            current++;
            return node;
        }
        
        throw new TypeError(token.type);
    }
    
    // AST 根節(jié)點(diǎn)
    let ast = {
        type: 'Program',
        body: [],
    };
    
    while (current < tokens.length) {
        // 遍歷所有節(jié)點(diǎn), 添加進(jìn)去
        ast.body.push(walk());
    }
    
    return ast;
}

the token array

[
    { 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: ')'        }, //<<< Closing parenthesis
    { type: 'paren',  value: ')'        }, //<<< Closing parenthesis
]

the tree

            (
          /   
        param 
      /    |   \
    "add"  2    (
                |
               param
             /  |  \
         "sub"  4   2

AST DONE!!

⌒(?>???<?)⌒ traverser

使用 visitor 訪問不同 AST 節(jié)點(diǎn), 在遇到不同 type 的節(jié)點(diǎn) call 不同的方法.

函數(shù)結(jié)構(gòu):

traverse(ast, {
    Program: {
        enter(node, parent) {},
        exit(node, parent) {}
    },
 
    CallExpression: {
        enter(node, parent) {},
        exit(node, parent) {},
    },

    NumberLiteral: {
        enter(node, parent) {},
        exit(node, parent) {},
    },
})

定義一個(gè) traverser 方法接收一個(gè) AST 和 visitor. 內(nèi)部有兩個(gè) functions

// 定義一個(gè) traverser 方法接收一個(gè) AST 和 visitor. 內(nèi)部有兩個(gè) functions
function traverser(ast, visitor) {

  // `traverseArray` 可以讓我們遍歷數(shù)組, 并調(diào)用我們定義的另一個(gè)方法 `traverseNode`
  function traverseArray(array, parent) {
    array.forEach(child => {
      traverseNode(child, parent);
    });
  }

  // `traverseNode` 接收 `node` 和它的 `parent` 節(jié)點(diǎn). 
  // 這樣我們就能傳 visitor 的兩個(gè) methods.
  function traverseNode(node, parent) {

    // 我們首先通過 type 看看 visitor 是否存在方法
    let methods = visitor[node.type];

    // 如果這個(gè)節(jié)點(diǎn)有 `enter` 方法, 我們就 enter 進(jìn)入
    if (methods && methods.enter) {
      methods.enter(node, parent);
    }

    // 接下來伍派,我們將按節(jié)點(diǎn) type 拆分
    switch (node.type) {
      // 從 level `Program` 開始, 它的 body 屬性的值是一個(gè) node 的 array,     
      // 我們用上面的方法遍歷
      // traverseArray 將依次調(diào)用 `traverseNode`江耀,所以我們在通過遞歸對樹進(jìn)行遍歷
      case 'Program':
        traverseArray(node.body, node);
        break;
      // 接下來, 我們對 CallExpression 的參數(shù) params 做同樣的事
      case 'CallExpression':
        traverseArray(node.params, node);
        break;
      // 遇到?jīng)]有子節(jié)點(diǎn)的 `NumberLiteral` 和 `StringLiteral`,
      // 直接 break 就可以了
      case 'NumberLiteral':
      case 'StringLiteral':
        break;
      // 不認(rèn)識的節(jié)點(diǎn), 直接報(bào)錯(cuò)
      default:
        throw new TypeError(node.type);
    }

    // 如果這個(gè)節(jié)點(diǎn)有 `exit` 方法, 我們就 exit 回退
    if (methods && methods.exit) {
      methods.exit(node, parent);
    }
  }

  // 調(diào)用 traverseNode, 傳入 AST, AST 的根節(jié)點(diǎn)沒有 parent 傳 null
  return traverseNode(ast, null);
}

?(??????????)? the transformer !!!

接下來,transfromer诉植。
我們的 transformer 將基于我們剛剛建立的 AST 創(chuàng)建一個(gè)新的 AST.

  ----------------------------------------------------------------------------
    Original AST                        Transformed 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'
                                     |           }]
                                     |         }
                                     |       }
                                     |     }]
                                     |   }
  ----------------------------------------------------------------------------

所以, 我們的 transformer接收 lisp 的 AST

function transformer(ast) {
  let newAst = {
    type: 'Program',
    body: [],
  };

  ast._context = newAst.body;

  // 用 AST 和 visitor 調(diào)用 traverser
  traverser(ast, {

    // 第一個(gè) visitor 接收所有的 `NumberLiteral`
    NumberLiteral: {
      // 通過 enter 訪問
      enter(node, parent) {
        // 我們會(huì)創(chuàng)建一個(gè)新的節(jié)點(diǎn), 也叫做 NumberLiteral, 然后把它放入 parent context
        parent._context.push({
          type: 'NumberLiteral',
          value: node.value,
        });
      },
    },

    // 接下來是 `StringLiteral`
    StringLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'StringLiteral',
          value: node.value,
        });
      },
    },

    // `CallExpression`.
    CallExpression: {
      enter(node, parent) {

        // 創(chuàng)建一個(gè)新節(jié)點(diǎn) `CallExpression` 并內(nèi)嵌一個(gè) `Identifier`.
        let expression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: node.name,
          },
          arguments: [],
        };

        // 接下來我們在原來的 `CallExpression` 節(jié)點(diǎn)上, 
        //定義一個(gè) context 指向 expression 的參數(shù)
        node._context = expression.arguments;

        // 如果父節(jié)點(diǎn)不是 `CallExpression`
        if (parent.type !== 'CallExpression') {

          // 用 `ExpressionStatement` 包一下 `CallExpression`, 
        //因?yàn)?`CallExpression` 在 JS 中是真的語句
          expression = {
            type: 'ExpressionStatement',
            expression: expression,
          };
        }

        // 最后, 把 (包裝過的) `CallExpression` 放入 parent 的 context.
        parent._context.push(expression);
      },
    }
  });

  // 返回剛剛生成的 new AST
  return newAst;
}

ヾ(〃^?^)?? code generator

最后一步, Code Generator, 將以遞歸的方式打印上一步得到的 AST 的每個(gè)節(jié)點(diǎn).

function codeGenerator(node) {

  // 通過node 的 type 屬性, 把 AST 轉(zhuǎn)為 code
  switch (node.type) {

    // 如果遇到 Program 節(jié)點(diǎn), 就遍歷 body 中的每個(gè)節(jié)點(diǎn),
    case 'Program':
      return node.body.map(codeGenerator)
        .join('\n');

    // 對于 `ExpressionStatement`, 我們在內(nèi)嵌表達(dá)式中遞歸的使用代碼生成器, 結(jié)束后加上分號
    case 'ExpressionStatement':
      return (
        codeGenerator(node.expression) + ';'
      );

    // 如果遇到 `CallExpression`, 就打印 `callee` 和左括號,
    // 把 node 中的 arguments 映射成數(shù)組, 用逗號 join 一下, 最后打印一個(gè)右括號
    case 'CallExpression':
      return (
        codeGenerator(node.callee) + '(' +  node.arguments.map(codeGenerator).join(', ') + ')'
      );

    // 遇到 `Identifier` 直接返回 `node`'s 名字.
    case 'Identifier':
      return node.name;

    // 對于 `NumberLiteral` 直接返回 `node` 的值.
    case 'NumberLiteral':
      return node.value;

    // 對 `StringLiteral` 給 `node` 的值加上引號.
    case 'StringLiteral':
      return '"' + node.value + '"';

    // 對于其他節(jié)點(diǎn), 報(bào)錯(cuò)
    default:
      throw new TypeError(node.type);
  }
}

點(diǎn)這里獲取代碼.

后面準(zhǔn)備逐一撕一下前端這些庫, 框架的面具, 都拾掇拾掇, 折騰折騰. 加油.

今天就到這里了.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末祥国,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子晾腔,更是在濱河造成了極大的恐慌舌稀,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灼擂,死亡現(xiàn)場離奇詭異壁查,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)剔应,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進(jìn)店門睡腿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人峻贮,你說我怎么就攤上這事席怪。” “怎么了纤控?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵挂捻,是天一觀的道長。 經(jīng)常有香客問我船万,道長刻撒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任耿导,我火速辦了婚禮声怔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘碎节。我一直安慰自己捧搞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著胎撇,像睡著了一般介粘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晚树,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天姻采,我揣著相機(jī)與錄音,去河邊找鬼爵憎。 笑死慨亲,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的宝鼓。 我是一名探鬼主播刑棵,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼愚铡!你這毒婦竟也來了蛉签?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤沥寥,失蹤者是張志新(化名)和其女友劉穎碍舍,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體邑雅,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡片橡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了淮野。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捧书。...
    茶點(diǎn)故事閱讀 39,953評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖录煤,靈堂內(nèi)的尸體忽然破棺而出鳄厌,到底是詐尸還是另有隱情,我是刑警寧澤妈踊,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站泪漂,受9級特大地震影響廊营,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜萝勤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一露筒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧敌卓,春花似錦慎式、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽癣防。三九已至,卻和暖如春掌眠,著一層夾襖步出監(jiān)牢的瞬間蕾盯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工蓝丙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留级遭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓渺尘,卻偏偏與公主長得像挫鸽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子鸥跟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容