手拉手教你實(shí)現(xiàn)一門編程語言 Enkel, 系列 3

本文系 Creating JVM language 翻譯的第三篇。

0. 自頂向下的方式

開發(fā)一門語言不是短期任務(wù)沈自,我將以自頂向下的視角來開發(fā)烘贴。為了避免陷入每個(gè)模塊的實(shí)現(xiàn)細(xì)節(jié)筷畦,我準(zhǔn)備先宏觀的描述每個(gè)模塊,然后在每個(gè)迭代症杏,我將慢慢加入更多的細(xì)節(jié)加以完善。

1. 代碼

作者原來的代碼是 Maven 工程瑞信,譯者這里改成了 Gradle(譯者熟悉 Gradle 并且覺得更為優(yōu)雅)厉颤,目錄結(jié)構(gòu)以及包名也做了些許改動(dòng),但是邏輯上都是等價(jià)的凡简。
讀者可根據(jù)喜好逼友,自行選擇精肃。

  • 譯者的 Gradle 工程地址
  • 作者的 Maven 工程地址

約定:每個(gè)章節(jié),涉及到代碼變更帜乞,譯者都會(huì)打 tag, tag 的名字和章節(jié)名字保持統(tǒng)一司抱,比如,當(dāng)前章節(jié)的代碼版本是 PART-3黎烈,以此類推习柠。

2. 功能

本章節(jié)我將為 Enkel 添加如下功能特性:

  • 聲明 int 或者 string 類型的變量
  • 打印變量
  • 簡單的類型推斷

示例如下:

//first.enk
 var five=5
 print five   
 var dupa="dupa"  
 print dupa  

下面我們將實(shí)現(xiàn)能最簡化版本,讓上述代碼可以運(yùn)行在 JVM 上照棋。

3. 用 ANTLR4 實(shí)現(xiàn)詞法分析&語法分析

從零開始實(shí)現(xiàn)一個(gè)詞法分析器是重復(fù)性很強(qiáng)的勞動(dòng)资溃。這里使用 Antlr (Antlr 4)實(shí)現(xiàn)。你需要做的只是創(chuàng)建一個(gè)語法規(guī)則文件烈炭,Antlr 幫你生成可以遍歷 AST 的基礎(chǔ)代碼溶锭。

本章語法規(guī)則文件為 Enkel.g4:

//header
grammar Enkel;

@header {
package com.bendcap.enkel.antlr;
}

//RULES
compilationUnit : ( variable | print )* EOF; //root rule - our code consist consist only of variables and prints (see definition below)
variable : VARIABLE ID EQUALS value; //requires VAR token followed by ID token followed by EQUALS TOKEN ...
print : PRINT ID ; //print statement must consist of 'print' keyword and ID
value : op=NUMBER
      | op=STRING ; //must be NUMBER or STRING value (defined below)

//TOKENS
VARIABLE : 'var' ; //VARIABLE TOKEN must match exactly 'var'
PRINT : 'print' ;
EQUALS : '=' ; //must be '='
NUMBER : [0-9]+ ; //must consist only of digits
STRING : '"'.*'"' ; //must be anything in qutoes
ID : [a-zA-Z0-9]+ ; //must be any alphanumeric value
WS: [ \t\n\r]+ -> skip ; //special TOKEN for skipping whitespaces

語法規(guī)則很簡單,需要注意的是:

  • EOF - 文件結(jié)束
  • 語法規(guī)則中的空格和 ; 是必須

Enkel.g4 定義好后符隙,可以運(yùn)行命令來生成后續(xù)需要的 Java 代碼:
antlr Enkel.g4

命令執(zhí)行完后暖途,會(huì)生成四個(gè)類:

  • EnkelLexer - 包含 Token 相關(guān)信息
  • EnkelParser - 解析器,Token 信息以及一些內(nèi)部類來做規(guī)則解析
  • EnkelListener - 當(dāng)訪問語法節(jié)點(diǎn)的時(shí)候膏执,提供回調(diào)函數(shù)
  • EnkelBaseListener - 空的 EnkelListener 實(shí)現(xiàn)

譯注: 如果你使用 Gradle 工程的話驻售,可以使用 ./gradlew :antlr:generateGrammarSource 來生成上述代碼。

其中最重要的是 EnkelBaseListener更米,這個(gè)類提供了遍歷 AST 時(shí)的回調(diào)函數(shù)欺栗,我們不用關(guān)心詞法分析和語法分析,Antlr 幫助我們屏蔽了這個(gè)過程征峦。

我們可以使用 javac *.java 編譯上述代碼迟几,來測(cè)試我們的規(guī)則正確性。

$ export CLASSPATH=".:$ANTLR_JAR_LOCATION:$CLASSPATH"
$ java org.antlr.v4.gui.TestRig Enkel compilationUnit -gui
var x=5
print x
var dupa="dupa"
print dupa
ctrl+D //end of file

上述輸入會(huì)生成如下的圖形化樹形結(jié)構(gòu)(抽象語法書的圖形化展示):


image

譯者注:可以使用 IDEA 插件 ANTLR 4 grammar plugin 來實(shí)現(xiàn)同樣的效果栏笆,或許更方便一些类腮。

image

4. 遍歷語法樹

EnkelListener 提供了我們遍歷語法樹的辦法。

EnkelTreeWalkListener.java
public class EnkelTreeWalkListener extends EnkelBaseListener {

    Queue<Instruction> instructionsQueue = new ArrayDeque<>();
    Map<String, Variable> variables = new HashMap<>();

    public Queue<Instruction> getInstructionsQueue() {
        return instructionsQueue;
    }

    @Override
    public void exitVariable(@NotNull EnkelParser.VariableContext ctx) {
        final TerminalNode varName = ctx.ID();
        final EnkelParser.ValueContext varValue = ctx.value();
        final int varType = varValue.getStart().getType();
        final int varIndex = variables.size();
        final String varTextValue = varValue.getText();
        Variable var = new Variable(varIndex, varType, varTextValue);
        variables.put(varName.getText(), var);
        instructionsQueue.add(new VariableDeclaration(var));
        logVariableDeclarationStatementFound(varName, varValue);
    }

    @Override
    public void exitPrint(@NotNull EnkelParser.PrintContext ctx) {
        final TerminalNode varName = ctx.ID();
        final boolean printedVarNotDeclared = !variables.containsKey(varName.getText());
        if (printedVarNotDeclared) {
            final String erroFormat = "ERROR: WTF? You are trying to print var '%s' which has not been declared!!!.";
            System.err.printf(erroFormat, varName.getText());
            return;
        }
        final Variable variable = variables.get(varName.getText());
        instructionsQueue.add(new PrintVariable(variable));
        logPrintStatementFound(varName, variable);
    }

    private void logVariableDeclarationStatementFound(TerminalNode varName, EnkelParser.ValueContext varValue) {
        final int line = varName.getSymbol().getLine();
        final String format = "OK: You declared variable named '%s' with value of '%s' at line '%s'.\n";
        System.out.printf(format, varName, varValue.getText(), line);
    }

    private void logPrintStatementFound(TerminalNode varName, Variable variable) {
        final int line = varName.getSymbol().getLine();
        final String format = "OK: You instructed to print variable '%s' which has value of '%s' at line '%s'.'\n";
        System.out.printf(format, variable.getId(), variable.getValue(), line);
    }
}

getInstructionsQueue 按照代碼順序返回指令蛉加。

下面我們可以注冊(cè) Listener:

SyntaxTreeTraverser.java
public class SyntaxTreeTraverser {
    public Queue<Instruction> getInstructions(String fileAbsolutePath) throws IOException {
        CharStream charStream = new ANTLRFileStream(fileAbsolutePath); //fileAbsolutePath - file containing first enk code file

        EnkelLexer lexer = new EnkelLexer(charStream);  //create lexer (pass enk file to it)

        CommonTokenStream tokenStream = new CommonTokenStream(lexer);

        EnkelParser parser = new EnkelParser(tokenStream);

        EnkelTreeWalkListener listener = new EnkelTreeWalkListener(); //EnkelTreeWalkListener extends EnkelBaseLitener - handles parse tree visiting events
        BaseErrorListener errorListener = new EnkelTreeWalkErrorListener(); //EnkelTreeWalkErrorListener - handles parse tree visiting error events

        parser.addErrorListener(errorListener);
        parser.addParseListener(listener);
        parser.compilationUnit(); //compilation unit is root parser rule - start from it!
        return listener.getInstructionsQueue();
    }
}

這里我提供了另一個(gè) Listener 來做異常處理:

EnkelTreeWalkErrorListener.java
public class EnkelTreeWalkErrorListener extends BaseErrorListener {
    @Override
    public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
        final String errorFormat = "You fucked up at line %d,char %d :(. Details:\n%s";
        final String errorMsg = String.format(errorFormat, line, charPositionInLine, msg);
        System.err.println(errorMsg);
    }
}

遍歷語法樹的 Listener 寫完后蚜枢,我們來測(cè)試一下。創(chuàng)建 Compiler 類作為入口针饥,輸入?yún)?shù)為我們 Enkel 語言的源文件路徑厂抽。

//Compiler.java
public class Compiler {

    public static void main(String[] args) throws Exception {
        new Compiler().compile(args);
    }

    public void compile(String[] args) throws Exception {
        //arguments validation skipped (check out full code on github)
        final File enkelFile = new File(args[0]);
        String fileName = enkelFile.getName();
        String fileAbsolutePath = enkelFile.getAbsolutePath();
        String className = StringUtils.remove(fileName, ".enk");
        final Queue<Instruction> instructionsQueue = new SyntaxTreeTraverser().getInstructions(fileAbsolutePath);
        //TODO: generate bytecode based on instructions
    }
}

下面我們既可以驗(yàn)證我們的 *.enk 文件了。目前的實(shí)現(xiàn)很簡陋丁眼,但是可以做如下事情:

  • 允許用 var x=1 或者 var x = "anthing" 來聲明變量
  • 允許用 print x 打印變量
  • 如果語法不符合規(guī)則筷凤,報(bào)錯(cuò)提示

下面我們創(chuàng)建示例 first.enk:

 var five=5
 print five   
 var dupa="dupa"  
 print dupa  

驗(yàn)證:

$java Compiler first.enk

OK: You declared variable named 'five' with value of '5' at line '1'.
OK: You instructed to print variable '0' which has value of '5' at line '2'.'
OK: You declared variable named 'dupa' with value of '"dupa"' at line '3'.
OK: You instructed to print variable '1' which has value of '"dupa"' at line '4'.'

first.enk 最后添加一行 void noFunctionsYet(), 再次驗(yàn)證,ErrorListener 可以檢測(cè)到錯(cuò)誤苞七,并且輸出如下提示信息:

You fucked up at line 1,char 0 :(. Details:
mismatched input 'void' expecting {<EOF>, 'var', 'print'}

5. 根據(jù) instructions queue 生成字節(jié)碼

Java 的 class 文件的包含的指令描述參見 JSE 文檔藐守。每一條指令包含如下結(jié)構(gòu):

  • 操作符(1 byte)- 指令
  • 可選的操作數(shù) - 指令的輸入

例: iload 5 (0x15 5)挪丢,從局部變量加載數(shù)據(jù),5 是局部變量數(shù)組的索引卢厂。
指令也可以實(shí)現(xiàn)操作數(shù)占的出棧入棧操作吃靠。
例如:

iload 3
iload 2
iadd

上述代碼分別從局部變量數(shù)組中加載索引為 3 和 2 的變量,此時(shí)棧中包含兩個(gè)數(shù)值足淆,iadd 指令將棧中兩個(gè)出棧巢块,相加,然后結(jié)果再次入棧巧号。

6. ASM

這里選用 ASM 來操作 Java 字節(jié)碼族奢。這樣可以不用關(guān)心非常底層的十六進(jìn)制數(shù)字,你只需要知道指令的名字丹鸿,ASM 會(huì)自動(dòng)幫你處理剩下的事情越走。

7. Instruction interface

SyntaxTreeTraverser 在遍歷 AST 的時(shí)候會(huì)把指令按照順序存儲(chǔ)到 instructionsQueue 中。我們做一次抽象靠欢,定義接口 Instruction

public interface Instruction {
    void apply(MethodVisitor methodVisitor);
}

接口的實(shí)現(xiàn)需要使用 MethodVisitor(ASM 提供的類) 來做一些代碼生成的操作廊敌。

//Compiler.java
public void compile(String[] args) throws Exception {
    //some lines deleted -> described in previous sections of this post
    final Queue<Instruction> instructionsQueue = new SyntaxTreeTraverser().getInstructions(fileAbsolutePath);
    final byte[] byteCode = new BytecodeGenerator().generateBytecode(instructionsQueue, className);
    saveBytecodeToClassFile(fileName, byteCode);
}
//ByteCodeGenerator.java
public class BytecodeGenerator implements Opcodes {
    public byte[] generateBytecode(Queue<Instruction> instructionQueue, String name) throws Exception {

        ClassWriter cw = new ClassWriter(0);
        MethodVisitor mv;
               //version ,      acess,       name, signature, base class, interfaes
        cw.visit(52, ACC_PUBLIC + ACC_SUPER, name, null, "java/lang/Object", null);
        {
            //declare static void main
            mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
            final long localVariablesCount = instructionQueue.stream()
                    .filter(instruction -> instruction instanceof VariableDeclaration)
                    .count();
            final int maxStack = 100; //TODO - do that properly

            //apply instructions generated from traversing parse tree!
            for (Instruction instruction : instructionQueue) {
                instruction.apply(mv);
            }
            mv.visitInsn(RETURN); //add return instruction

            mv.visitMaxs(maxStack, (int) localVariablesCount); //set max stack and max local variables
            mv.visitEnd();
        }
        cw.visitEnd();

        return cw.toByteArray();
    }
}  

由于目前 Enkel 不支持 方法,類以及作用域等概念门怪,因?yàn)榫幾g后的類直接繼承自 Object, 包含一個(gè) main 函數(shù)骡澈。MethodVisitor 需要提供局部變量以及棧的深度。然后我們遍歷 instructionQueue 的每條指令來生成對(duì)應(yīng)的字節(jié)碼掷空。目前我們只有兩種指令(變量聲明以及打印語句):

//VariableDeclaration.java
public class VariableDeclaration implements Instruction,Opcodes {
    Variable variable;

    public VariableDeclaration(Variable variable) {
        this.variable = variable;
    }

    @Override
    public void apply(MethodVisitor mv) {
        final int type = variable.getType();
        if(type == EnkelLexer.NUMBER) {
            int val = Integer.valueOf(variable.getValue());
            mv.visitIntInsn(BIPUSH,val);
            mv.visitVarInsn(ISTORE,variable.getId());
        } else if(type == EnkelLexer.STRING) {
            mv.visitLdcInsn(variable.getValue());
            mv.visitVarInsn(ASTORE,variable.getId());
        }
    }
}

這里值得注意的是肋殴,我們已經(jīng)添加了簡單的類型推斷。我們會(huì)根據(jù)變量的實(shí)際類型進(jìn)行類型推斷坦弟。針對(duì)不同類型我們需要調(diào)用 ASM 不同的方法:

  • visitInsn - 第一個(gè)參數(shù)是操作符护锤,第二個(gè)是操作數(shù)
  • BIPUSH - 把一個(gè) byte(integer) 入棧
  • ISTORE - int 類型的值出棧,并存儲(chǔ)到局部變量中,需要指定局部變量的索引
  • ASTORE - 和 ISTORE 功能類似酿傍,但是數(shù)據(jù)類型是索引

打印語句的代碼生成如下:

//PrintVariable.java
public class PrintVariable implements Instruction, Opcodes {
    public PrintVariable(Variable variable) {
        this.variable = variable;
    }

    @Override
    public void apply(MethodVisitor mv) {
        final int type = variable.getType();
        final int id = variable.getId();
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        if (type == EnkelLexer.NUMBER) {
            mv.visitVarInsn(ILOAD, id);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
        } else if (type == EnkelLexer.STRING) {
            mv.visitVarInsn(ALOAD, id);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
    }
}
  • GETSTATIC - 從java.lang.System.out獲得靜態(tài)屬性烙懦,類型是 java.io.PrintStream
  • ILOAD - 把局部變量入棧,id 是局部變量的索引
  • visitMethodInsn - 訪問方法指令
  • INVOKEVIRTUAL - 觸發(fā)實(shí)例方法 (調(diào)用 out 的 print 方法赤炒,該方法接受一個(gè)參數(shù)為整數(shù)類型氯析,返回為空)
  • ALOAD - 和 ILOAD 類型,但是數(shù)據(jù)類型是引用

8. 生成字節(jié)碼

cw.toByteArray(); 執(zhí)行后可霎,ASM 創(chuàng)建一個(gè) ByteVector 實(shí)例并把所有的指令加入進(jìn)去魄鸦。Java class 文件的結(jié)構(gòu)為:

//https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1
ClassFile {
    u4             magic; //CAFEBABE
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1]; //string constants etc...
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

魔數(shù)(Magic Number)是 0xCAFEBABE。由于 Enkel 目前不支持字段癣朗,屬性,父類或者接口旺罢,因此我們這里主要描述了 method_info旷余。

9. 寫入字節(jié)碼到文件

JVM 規(guī)范要求我們 .class 文件的名字必須和類型相同绢记。所以我們這里保持文件名字和類名一直,僅替換后綴 (*enk -> *.class)正卧。

//Compiler.java
private static void saveBytecodeToClassFile(String fileName, byte[] byteCode) throws IOException {
        final String classFile = StringUtils.replace(fileName, ".enk", ".class");
        OutputStream os = new FileOutputStream(classFile);
        os.write(byteCode);
        os.close();
    }

10. 驗(yàn)證字節(jié)碼

我們可以使用 JDK 自帶的 javap 工具來驗(yàn)證生成的字節(jié)碼的正確性蠢熄。

$ $JAVA_HOME/bin/javap -v file

Classfile /home/kuba/repos/Enkel-JVM-language/file.class
  Last modified 2016-03-16; size 335 bytes
  MD5 checksum bcbdaa7e7389167342e0c04b52951bc9
public class file
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Utf8               file
   #2 = Class              #1             // file
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               Test.java
   #6 = Utf8               main
   #7 = Utf8               ([Ljava/lang/String;)V
   #8 = Utf8               java/lang/System
   #9 = Class              #8             // java/lang/System
  #10 = Utf8               out
  #11 = Utf8               Ljava/io/PrintStream;
  #12 = NameAndType        #10:#11        // out:Ljava/io/PrintStream;
  #13 = Fieldref           #9.#12         // java/lang/System.out:Ljava/io/PrintStream;
  #14 = Utf8               java/io/PrintStream
  #15 = Class              #14            // java/io/PrintStream
  #16 = Utf8               println
  #17 = Utf8               (I)V
  #18 = NameAndType        #16:#17        // println:(I)V
  #19 = Methodref          #15.#18        // java/io/PrintStream.println:(I)V
  #20 = Utf8               \"dupa\"
  #21 = String             #20            // \"dupa\"
  #22 = Utf8               (Ljava/lang/String;)V
  #23 = NameAndType        #16:#22        // println:(Ljava/lang/String;)V
  #24 = Methodref          #15.#23        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #25 = Utf8               Code
  #26 = Utf8               SourceFile
{
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: bipush        5
         2: istore_0
         3: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
         6: iload_0
         7: invokevirtual #19                 // Method java/io/PrintStream.println:(I)V
        10: ldc           #21                 // String \"dupa\"
        12: astore_1
        13: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
        16: aload_1
        17: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        20: return
}

11. 運(yùn)行 Enkel

下面我們來運(yùn)行第一個(gè) Enkel 代碼:

 var five=5
 print five   
 var dupa="dupa"  
 print dupa  

如果一切順利的話,會(huì)有如下輸出:

$java Compiler first.enk
$java first
5
"dupa"

譯者注:上述成品代碼托管在 Github

預(yù)告:下一節(jié)給 Enkel 增加一大坨特性炉旷,并定義好規(guī)范签孔,方便后續(xù)迭代實(shí)現(xiàn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末窘行,一起剝皮案震驚了整個(gè)濱河市饥追,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌罐盔,老刑警劉巖但绕,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異惶看,居然都是意外死亡捏顺,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門纬黎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幅骄,“玉大人,你說我怎么就攤上這事本今〔矗” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵诈泼,是天一觀的道長懂拾。 經(jīng)常有香客問我,道長铐达,這世上最難降的妖魔是什么岖赋? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮瓮孙,結(jié)果婚禮上唐断,老公的妹妹穿的比我還像新娘。我一直安慰自己杭抠,他們只是感情好脸甘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著偏灿,像睡著了一般丹诀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天铆遭,我揣著相機(jī)與錄音硝桩,去河邊找鬼。 笑死枚荣,一個(gè)胖子當(dāng)著我的面吹牛碗脊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播橄妆,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼衙伶,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了害碾?” 一聲冷哼從身側(cè)響起矢劲,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蛮原,沒想到半個(gè)月后卧须,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡儒陨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年花嘶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹦漠。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡椭员,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出笛园,到底是詐尸還是另有隱情隘击,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布研铆,位于F島的核電站埋同,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏棵红。R本人自食惡果不足惜凶赁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望逆甜。 院中可真熱鬧虱肄,春花似錦、人聲如沸交煞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽素征。三九已至集嵌,卻和暖如春萝挤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背纸淮。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來泰國打工平斩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留亚享,地道東北人咽块。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像欺税,于是被迫代替她去往敵國和親侈沪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

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

  • 原文地址:C語言函數(shù)調(diào)用棧(一)C語言函數(shù)調(diào)用棧(二) 0 引言 程序的執(zhí)行過程可看作連續(xù)的函數(shù)調(diào)用晚凿。當(dāng)一個(gè)函數(shù)執(zhí)...
    小豬啊嗚閱讀 4,610評(píng)論 1 19
  • 這些天幣市的持續(xù)大跌亭罪,大部分主流幣跌破前期低點(diǎn),即使在高溫之下也讓人感受到陣陣寒意歼秽。熊市之下也只能多復(fù)盤多總結(jié)了应役。...
    獨(dú)自成長閱讀 401評(píng)論 1 1
  • 這是米米,我第一份工作認(rèn)識(shí)的同事≡锟辏現(xiàn)在是我的好朋友之一箩祥,第一人發(fā)現(xiàn)這個(gè)世界上還有和自己呼吸節(jié)奏那么相似的人。 白羊...
    行走布丁的葵花閱讀 105評(píng)論 0 0
  • 書接上文肆氓,第一天因同路朋友提議去了友誼關(guān)袍祖,因此第二天決定去德天瀑布再轉(zhuǎn)去云南石林。 早上在大新縣招待所門口吃了一晚...
    苦丁茶苦苦閱讀 1,072評(píng)論 0 0