返回首页

introductionnbsp;
本文介绍了C#程序员的编程语言实现的基本概念。它旨在提供一个快速实施的编程语言,使用的例子包括一个算术计算器和一个简单的JavaScript interpreter.nbsp的概念的概述;
本文是一个开放源码库,写在C#4.0称为拼图。您可以下载的最先进的{A}拼图版本。
拼图是一个高效率和强大的分析引擎,以及大量样品的语法和评估。拼图解析库是进化 NBSP使用的解析器。
拼图库根据麻省理工学院的开放原始码授权许可。如果你需要另一个许可证,请联系我{A3的}。如果你最终使用或滥用拼图,我喜欢听到这个消息!NET编译器内置。
在我们开始之前,我应该指出,如果你正在寻找关闭在C货架解释,你应该考虑System.CodeDom.Compiler命名空间和一个或多个下列组件:Microsoft.CSharpnbsp;Microsoft.Jscriptnbsp;Microsoft.VisualBasicnbsp;
这些组件提供的CodeDomProvider从派生类(如CSharpCodeProvider)的,你可以用它来编译在运行时的大会。
下面的函数演示如何使用的CodeDomProvider动态生成一个程序集:

public static Assembly CompileWithProvider(CodeDomProvider provider, params string[] lines)

{

    var param = new System.CodeDom.Compiler.CompilerParameters();

    var result = provider.CompileAssemblyFromSource(param, lines);

    foreach (var e in result.Errors)

        Console.WriteLine("Error occured during compilation {0}", e.ToString());

    if (result.Errors.Count > 0)

	return null;

    else

        return result.CompiledAssembly;

}

见更多的例子,如何使用内置NET中compilers.nbsp的文件CodeDOMCompilers.cs。;
说,我相信你在这里,因为你想了解实施的编程语言的黑色艺术,因此,继续勇敢的读者!解剖语言Toolnbsp;
大多数语言工具遵循相同的基本架构:标记化(可选)?这个阶段也被称为词法分析,词法分析和扫描。在这个阶段,一个字符串转换成一个字符串标记(也称为语意)。这个阶段是必要的(如某些类型的解析器,是LALR),但不是别人(如聚乙二醇)。没有标记化阶段的分析器(如拼图)被称为扫描器少parsers.nbsp;??解析器转换的一个记号,或将字符树称为解析tree.nbsp结构的线性序列;??树变压器(可选)修改简化后steps.nbsp解析树;??树访问者访问树中的每个节点,并执行一些动作,或创建一个新的数据structure.nbsp;
会发生什么定义在节点访问语言工具的类型。例如: ??解释器转换成运行时间值的节点,或执行一个原始action.nbsp;??编译器转换成机器可执行representation.nbsp的节点;漂亮的打印机?转换为人类可读的形式,如ASCII或HTML节点。??翻译转换成一种新的编程language.nbsp在源代码表示的节点;类型检查??节点转化成一个表达式的类型的代表。这是一个的形式ofnbsp;抽象解释。部分评估优化阶段,其中某些节点在树替换他们的评价(例如,编译时表达式)NBSP??;
这是一个有用的简化:语言工具操纵的树木。parsingnbsp;
分析,句法分析,告诉我们一个输入字符串是否匹配一个特定的语法形式(语法),并分成句法成分(句法短语或条款)NBSP解析树的输入。;
。NET Framework中没有做解析框工具,但也有大量的第三方分析工具选择。一些比较流行的工具是ANTLR的,YACC的,和野牛。这些工具分析器发电机,这意味着theynbsp;生成一个解析器定义解析器(语法)的源代码。
使用代码生成器的问题,是相当陡峭的学习曲线。他们每个人都有自己的语法和规则。我要实现自己手写的分析器之前,我可以了解如何使用这些工具。那时已经太晚了,我迷上了由手工编写解析器。
在这篇文章中,我使用C#解析库,我写的,叫拼图。我选择使用手写的分析器,因为它是更容易理解和调试。拼图是memoizing递归下降回溯的PEG解析器。这是一口让我们打破这意味着什么: {A4纸}使用了一套相互递归程序,认识到语言的语法元素。 一回跟踪分析器将试图解析,以一套规则,在面临选择时直到有succeeds.nbsp,;一个memoizing解析器解析器缓存(memoizes)的解析用一个查找表的规则,从而提高了算法的时间复杂的中间结果。 Memoizing的PEG解析器也被称为{A5的}。一个PEG({A6的})解析器承认每个规则直接对应一个模式匹配算法的语法。更常见的替代聚乙二醇是LR的预测分析器(其中有很多),但他们更复杂。
这一切​​都不是特别重要的,当你第一次约parsing.nbsp学习一直是我的经验,这些类型的解析器最密切的对应我如何解析器应该work.nbsp的直觉;grammarsnbsp;
语法是一种语言的语法的正式定义。写作语法是有点像写一个正则表达式。语法包括其他规则使用规则的运营商(也称为组合器)中定义的一个或多个规则。每个规则在语言描述了一个特定的语法元素称为phrase.nbsp;
在拼图库,语法为聚乙二醇(解析表达式语法)表示。在PEG语法,每个规则定义一个解析器特定句法element.nbsp的;
一些忘却时间:如果您先前已经了解到,关于上下文无关文法(CFG)的聚乙二醇在每个规则描述如何匹配字符串是不同的,而不是如何识别它们。一个含义是,聚乙二醇可以零宽度的规则,是毫不含糊的。 PEG和一个CFG之间的差异是细微但很重要的。
每个规则是从规则派生的类的一个实例。其作用是识别语言的句法元素(称为任期ornbsp;短语)。这肯定是做的功能匹配()返回一个布尔值,指示是否匹配是成功的。 MATCH函数接受一个ParserState类的实例,它拥有输入字符串,在输入字符串中的位置,并解析tree.nbsp,,;
您可以创建实例从的语法class.nbsp的静态成员函数的规则;{C}
您可以建立复合规则,获得成功,只有一个孩子的规则序列匹配成功使用加号("quot ;)运营商。
var rule = Grammar.MatchString("cat") + Grammar.MatchString("fish")

rule.Match("catnip"); // false

rule.Match("dogfish"); // false

rule.Match("catfish"); // true 
你可以建立复合规则,尝试任何匹配的一系列规则,使用管道(quot; | operator.nbsp quot ;) {C3的}
规则运营商可以结合,以创建更复杂的规则。{的C4}
规则可以被定义为可选功能,使用的Grammar.Opt():{C5的}
重复的规则也可以使用Grammar.ZeroOrMore()和Grammar.OneOrMore()创建。例如: 创建解析树
只要承认不是一个字符串是否属于一些语法也不是特别有用的,当写一个编程语言工具。我们真正需要的是一个数据结构,代表的input.nbsp的结构;
的拼图允许这将嵌入Grammar.Nodenbsp语法规则。这些规则相关的规则,如果匹配成功添加一个新的节点实例解析树。调用Rule.Parse()函数将返回一个解析节点列表,每一个的解析tree.nbsp的的根;
节点规则有被命名为使用Rule.SetName()函数(不像其他规则的名称是可选的)。这个名字被用来作为相关节点在节点树的标签。
下面的代码定义了一个简单的语法解析的话:{C7-}
上述程序的输出是:{C8的}Memoizing中间结果(缓存)
有些递归下降解析器可以采取极长的解析一定的投入。解决的办法是在查表中间的解析结果缓存。这种技术被称为memoization的。
在拼图库,所有NodeRule比赛结果是在存储在thenbsp字典缓存; ParserState object.nbsp;
如果设置NodeRule.UseCache常数为false,你可以看到解析运行测试在JavaScriptTests类的某些语法需要多长时间。简化语法:语法类派生
当定义语法,以下几点是特别讨厌:需要添加前缀Grammarnbsp的在前面的规则创建功能。需要明确设置节点规则的名称。
您可以解决这些问题通过在派生从Grammarnbsp类定义的所有规则,每个规则作为一个公共的静态字段声明。然后,您可以使用静态初始化InitGrammar()功能自动与field.nbsp关联的每个规则指定名称;
你可以简化使用的语法在前面的例子如下:
public class SentenceGrammar : Grammar

{

    public static Rule word = Node(Pattern(@"\w+"));

    public static Rule ws = Pattern(@"\s+");

    public static Rule eos = CharSet("!.?");

    public static Rule sentence = Node(ZeroOrMore(word | ws) + eos);

    public static Rule sentences = OneOrMore(sentence + Opt(ws));



    static SentenceGrammar() {

        Grammar.InitGrammar(typeof(SentenceGrammar));

    }

}
递归规则
变量或字段定义的规则不能提及自己或一个尚未定义的规则。这将导致在规则定义中的空引用。例如,下面的语法将产生一个类型初始化过程中的错误: {C10的}
解决方案之一是使用递归()的规则,采取一个lambda表达式返回在运行time.nbsp的表达;{C11的}避免左递归Rulesnbsp;
在PEG语法,递归规则不允许在序列最左边的位置。这些被称为quot;左recursivequot;规则,并会导致到进入无限loop.nbsp的解析器;
例如,考虑承认函数调用语法(例如,F(X)或F(X)(Y)(Z)): {C12的}
进入一个无限循环,并会导致堆栈溢出异常。解决办法是重写递归规则作为迭代规则。
static Rule PostfixExpr = Node(UnaryExpr + ZeroOrMore(PostfixOp));

承认此规则所需的表达式,但不幸的是产生一个解析树,可以引入一个计算器或编译器的意外复杂。你可以解决使用后解析改造通。这是讨论中的quot;写树Transformerquot;节。一个简单的算术Grammarnbsp;
到目前为止我们了解到,这里的一切放在一起,是一个简单的语法解析算术表达式:
class ArithmeticGrammar : SharedGrammar

{

    new public static Rule Integer = Node(SharedGrammar.Integer);

    new public static Rule Float = Node(SharedGrammar.Float);

    public static Rule RecExpr = Recursive(() => Expression);

    public static Rule ParanExpr = Node(CharToken('(') + RecExpr + WS + CharToken(')'));

    public static Rule Number = (Integer | Float) + WS;

    public static Rule PrefixOp = Node(MatchStringSet("! -  "));

    public static Rule PrefixExpr = Node(PrefixOp + Recursive(() => SimpleExpr));

    public static Rule SimpleExpr = PrefixExpr | Number | ParanExpr;

    public static Rule BinaryOp = 

      Node(MatchStringSet("<= >= == != << >> && || < > & | + - * % / ^"));

    public static Rule Expression = Node(SimpleExpr + ZeroOrMore(BinaryOp + WS + SimpleExpr));

    static ArithmeticGrammar() { InitGrammar(typeof(ArithmeticGrammar)); }

}

您可能已经注意到,我这里所骗使用SharedGrammarnbsp基类。这个类定义了一些额外的共同规则和规则操作,如WS,整数,浮点数,和CharToken()。我会离开它给你捅在代码周围看到什么是可用的。算术语法解析Treenbsp; NBSP评价;
一旦通过解析一个字符串生成的解析树,我们希望它转换成一个值。这个过程被称为评价。
要做到这一点,我们创建一个新的的类称为ArithmeticEvaluator,有两个功能:EVAL(字符串s) 和Eval(节点n)。
为方便起见,这两个函数返回一个动态值。当我们有dynamicnbsp申报值;类型,它告诉编译器跳过类型检查。相反,所有行动都解决在运行time.nbsp;
class ArithmeticEvaluator

{

    public static dynamic Eval(string s)

    {

        return Eval(Grammars.ArithmeticGrammar.Expression.Parse(s)[0]);

    }



    public static dynamic Eval(Node n)

    {

        switch (n.Label)

        {

            case "Number":      return Eval(n[0]);

            case "Integer":     return Int64.Parse(n.Text);

            case "Float":       return Double.Parse(n.Text);

            case "PrefixExpr":

                switch (n[0].Text)

                {

                    case "-": return -Eval(n[1]);

                    case "!": return !Eval(n[1]);

                    case " ": return  Eval(n[1]);

                    default: throw new Exception(n[0].Text);

                }

            case "ParanExpr":   return Eval(n[0]);

            case "Expression":

                switch (n.Count)

                {

                    case 1: 

                        return Eval(n[0]);

                    case 3: 

                        switch (n[1].Text)

                        {

                            case "+": return Eval(n[0]) + Eval(n[2]);

                            case "-": return Eval(n[0]) - Eval(n[2]);

                            case "*": return Eval(n[0]) * Eval(n[2]);

                            case "/": return Eval(n[0]) / Eval(n[2]);

                            case "%": return Eval(n[0]) % Eval(n[2]);

                            case "<<": return Eval(n[0]) << Eval(n[2]);

                            case ">>": return Eval(n[0]) >> Eval(n[2]);

                            case "==": return Eval(n[0]) == Eval(n[2]);

                            case "!=": return Eval(n[0]) != Eval(n[2]);

                            case "<=": return Eval(n[0]) <= Eval(n[2]);

                            case ">=": return Eval(n[0]) >= Eval(n[2]);

                            case "<": return Eval(n[0]) < Eval(n[2]);

                            case ">": return Eval(n[0]) > Eval(n[2]);

                            case "&&": return Eval(n[0]) && Eval(n[2]);

                            case "||": return Eval(n[0]) || Eval(n[2]);

                            case "&": return Eval(n[0]) & Eval(n[2]);

                            case "|": return Eval(n[0]) | Eval(n[2]);

                            default: throw new Exception("Unrecognized operator " + n[1].Text);

                        }

                    default: 

                        throw new Exception(String.Format(

                          "Unexpected number of nodes {0} in expression", n.Count));

                }

            default:

                throw new Exception("Unexpected type of node " + n.Label);

        }

    }

}
写一个JSON Parsernbsp;
JSON表示的JavaScript Object Notation。这是一个经常被用于文本数据表示语言JavaScript语言的一个子集。它有一个类似的结构到XML。
在拼图库,也可以从字符串动态创建类的JSONObject从DynamicObject而得。这意味着,我们可以写类似: {C16的}
在这篇文章中,JSONObject的实施,最有趣的部分是parse()函数。{C17的}
为了实现解析功能,我们首先需要定义一个JSON的语法。{C18的}
接下来,我们需要编写一个函数,从语法树转换成一个JSONObject。作为ArithmeticEvaluator的例子,我们将按照相同的基本形式。我们会写只有一个Eval()函数可以返回任何有效的JSON类型(如数字,字符串,数组等)的argument.nbsp标签;{C19的}编写一个简单的JavaScript解释器
的最简单的一种解释是比周围的评价函数的包装。与JSON和算术评估,评估一种编程语言,有管理变量和函数名。编写一个JavaScript语法
这是有道理的JSON的语法来自一个JavaScript语法。一些规则将被改写,所以可以在对象和数组放在任意表达式定义对象和数组常量的规则,不只是文字,如。
下面是JavaScript的语法:{C20的}编写源代码打印机
鉴于从JavaScript分析器生成的解析树,是最简单的工具,我们可以建立一个源代码打印机。这是一个有用的中间步骤,当你正在开发的验证解析器作为expected.nbsp工作的语言;
一个源代码,打印机(或漂亮的打印机)打印格式表示的输入助攻。在文本编辑器,这可能是有用的,或做源到源转换时。
拼图库中的打印机类有利于编写自定义源转换。从打印机派生,你只需要重写Print()函数,然后可以很容易地产生上那种节点received.nbsp的输出根据;
下面的代码片断采取从JavaScriptSourcePrinter类。{C21的}写一个树变压器
JavaScript分析器效果很好,虽然解析树的某些部分将很难在评价工作: 链接的二进制表达式是不能分开的。例如,当BinaryExpr规则遇到3 4 * 5,将会把一个节点与5岁以下儿童(3,4,5)(3,(4 * 5)),会更容易工作with.nbsp;链接后缀的运营商都没有分开。例如,F(1)"; helloquot;场将被解析的PostfixExpr规则(F,"helloquot,场)quot;(1),而会更容易,如果它是(((F,(1)),"helloquot;]),场)。
为了简化评估,以及其他可能的,从名为JSTransformernbsp TreeTransformer派生的工具类;可以发现的拼图库中。这种变压器的任务数: 后缀表达式转换成表达以下类型之一:FieldExpr表达一个quot;??quot;和一个标识符。例如,"myobject.myfieldquot;IndexExpr??后跟表达式索引操作。例如,"myArray的[索引]"。CallExpr??后跟表达式参数列表。例如,"MYFUNC(arg0的,arg1的)"。 MethodCallExpr??一个FieldExpr后跟一个参数列表。例如,"myObject的。 MYFUNC(arg0的,arg1的)"。 分离长二进制表达式,根据优先规则。转换命名的函数,将变量声明的形式(例如,函数f(x){})。VAR F =功能(X){} 转换转化为循环while循环。重写特殊的赋值操作符,使评估只考虑基本的赋值运算符。例如,"= bquot;成为"A = A bquot;替换节点只有一个孩子,孩子node.nbsp;管理的Environmentnbsp;
当你一种编程语言编写的一个计算器,你有跟踪名称(例如,函数名,变量名,参数名)的值。的值绑定到名字统称环境。拼图,环境管理由称为VarBindings类。你可以看到一个例子;其使用在Evaluatornbsp class.nbsp;
VarBindings类是递归定义的关联列表类。它包含一个名称及其关联的值,连同到另一个VarBindings类的指针。这种环境的代表性,是不是特别有效,但使用起来非常方便。
在JavaScript中,声明变量时,它被添加到变量绑定列表。当1 quot; blockquot;超出范围,宣布在该范围内的所有名称绑定。拼图,这样做是在功能称为quot; EvalScopedquot; EvalScoped功能需要另一个函数作为参数。它需要一个快照的环境,执行功能,然后恢复环境。{C22,}
在JavaScript中,一个新的名字第一次使用时,具有约束力的自动创建。这可能不会是一个伟大的语言设计的决定,因为它增加了程序员的错误的机会,但我们尊重它在我们的实现。这种逻辑被称为AddOrCreateBinding()函数处理。JavaScript函数
JSON的不同,执行一个JavaScript解释器时,我们不得不考虑什么数据结构来代表功能。在JavaScript中,函数是封。这意味着该功能可以在函数外声明的变量。这些变量被称为自由变量。
实施封闭,最简单的方法之一是使用函数值宣布的时刻,从环境的一个副本。当函数被应用到它的参数(即,调用),暂时取代目前评估的环境与function.nbsp存储版本;
这里显示拼图中所使用的JavaScript函数的实现:{C23的}return语句
当评估一系列的陈述和returnnbsp;语句时,你需要存储返回值和退出封闭的功能。
在JavaScript的评估,我们设置一个标志(isReturning)。每当一个复合语句执行(例如,for循环,而循环等),以决定是否应跳过执行后报表,这个标志被选中。我们选择了这种方法,因为它是简单易懂,容易implement.nbsp;
一个更简单的方法将抛出一个异常。然而,这已调试困难,将有一个显着的性能产生负面影响。
这个问题的一个更有效的方法是重写的解析树,使只有一个quot; returnquot;在函数结束时的发言。这是有点复杂,因为它需要增加额外的变量,重写任何循环的条件,但将移动的计算器功能,将变压器的复杂性。评价职能
JavaScript的评价功能在结构上是相似的JsonObject.Eval()有以下不同的功能:转化为节点树的形式,这是一个有点容易评估变量管理在VarBindings对象的一个新的数据类型(JSFunction)
评价函数太长,在这里列出,但在这里是一个有代表性的片断:{C24的}扩展的原始设置
评估包括只实现了基本的运营商,并拥有一个单一的内置功能quot; alertquot。内置功能实现使用JSPrimitive类和初始化计算器时,加入到全球环境。
public JavaScriptEvaluator()

{

    // This is where you could add all sorts

    // of primitive objects and functions. Or don't. Fine.



    AddBinding("alert", new JSPrimitive(args => { Console.WriteLine(args[0]); }));

}
一个表达式树编译器的JavaScript
表达System.Linq.Expressions命名空间的类(也称为一个表达式树)是一种方便的方式在运行时动态地创建新的功能。我们可以通过解析树转换成一个表达式树,然后使用的Expression.Compile()函数来创建在运行时可以使用DynamicInvoke()NBSP执行代表。
生成表达式树是类似的评价,除,而不是为每个节点返回一个动态值,我们返回的表达类的一个实例。表达式编译器可以从的ExpressionCompiler实用类。
JavaScript的表达拼图库编译器的样品,JavaScriptExpressionCompiler, 提供了两个编译功能,需要一个字符串,以及其他需要1 Node.nbsp;
public static Delegate CompileLambda(string s)

{

    var nodes = JavaScriptGrammar.AnonFunc.Parse(s);

    return CompileLambda(nodes[0]);

}



public static Delegate CompileLambda(Node n)

{

    n = JavaScriptTransformer.Transform(n);

    var compiler = new JavaScriptExpressionCompiler();

    var expr = (LambdaExpression)compiler.ToExpr(n);

    if (expr == null) return null;

    return expr.Compile();

}

请注意,如评价职能,变换用于简化解析树前进入了quot; ToExprquot;表达式转换成每个节点的功能。下一步
有一些不是在JavaScript解释器和表达式编译器实现的JavaScript功能。为进一步研究,可以延长提供的样品,以实现更多的功能或添加,您可能会发现有趣的编程语言的特点。
希望你有足够的教训,你就可以开始创建自己的languages​​.nbsp试点;
有,你可能会发现有趣的拼图库中包含的其他几个样品:ILCompiler.cs??的IL汇编代码的执行。SchemeExpressionCompiler.cs?一个简单的表达式编译器,一个Scheme语言的一小部分。计划是一个Lisp方言。??CatEvaluator.cs包含猫语言计算器,一个简单的功能基于堆栈的language.nbsp; 最后Wordsnbsp;
本文实施编程语言只是表面的划痕。如果你喜欢这篇文章,想了解更多有关编程语言实现的,你可能有兴趣知道,我工作的一本书工作标题quot;实施编程语言,在C#quot;请电子邮件} {A3的我,如果你想了解更多信息,成为一个测试的审稿人,或只是为了显示您的支持。感谢NBSP的 !; historynbsp; 2011年10月22日 - 第一次提交。10月23日,2011 - 从下载包中删除多余的文件,并添加缺少的环节,下载。2011 - 10月28日,制造了大量的编辑一个由Tracey休斯敦奇妙的详细审查。感谢特雷西!十二月27,2011 - {A8的} NBSP一些编辑详细审查;感谢大卫,对不起了我这么久,以增加他们NBSP。

回答

评论会员:弥敦道艾伦 时间:2012/02/06
非常好,我们一定要降低解析酒吧和避免这么多的即席在这个空间中的代码
评论会员:umitcs 时间:2012/02/06
mteşekkirim
评论会员:游客 时间:2012/02/06
shaheen_mix:我可能不打算使用的概念,在我平时做的工作,但它是一个伟大的文章fecking。清爽,易于阅读。干得好|和ThorstenBruning:不错{S0}
Monjurul哈比卜
评论会员:。非常凉爽,感谢 时间:2012/02/06
Wooters:
|不错的工作
评论会员:GanesanSenthilvel 时间:2012/02/06
大文章,感谢
评论会员:。杰夫齐尔切奥 时间:2012/02/06
好文章在C#4.0
评论会员:米哈伊MOGA 时间:2012/02/06
尼斯{S0的}
评论会员:帕斯卡尔Ganaye 时间:2012/02/06
!!!很不错的
评论会员:克里斯托弗diggins 时间:2012/02/06
大文章。请保持它
评论会员:游客 时间:2012/02/06
yroman:如果您尝试编译:{C27的}它只返回8有超过3个节点的问题与表达。{C28的}语法没有算符优先的概念,或者不
克里斯托弗Diggins
评论会员:游客 时间:2012/02/06
良好的渔获物。没有文法的处理运算符优先级,而不是我做树变压器。但是有没有树的变压器,但C#语法。{A9的}
yroman:(1 2 3)==

"表达":
开关(n.Count)
例1:
返回的Eval(N [0]);
案例3:??/ /
开关(N [1]。文本)
{
}

修改11月3日'11
评论会员:游客 时间:2012/02/06
帕斯卡尔Ganaye:哪语法{A9的}
帕斯卡尔Ganaye:ArithmeticEvaluator

公共静态无效的测试()
{
 60; 测试(1 2 3");
}

=错误运行测试1 2 3,结果发生意外的数字表达的节点5
:克里斯托弗Diggins:本文仅描述拼图的一小部分。这也许是我所见过的最好的一块代码。我不能相信这么几行多少
评论会员:kasunt 时间:2012/02/06
顺便说一句,你看看:{A11}

我很想能够创建域特定语言和编辑,编译和调试在Visual Studio
评论会员:克里斯托弗Diggins 时间:2012/02/06
非常感谢!听到这实在是令人振奋。 {S0的}
{A9的}
评论会员:sachinsharmaa102 时间:2012/02/06
大文章

{A13号}
评论会员:克里Diggins 时间:2012/02/06
谢谢你
{A9的}
评论会员:GanesanSenthilvel 时间:2012/02/06
先生伟大的文章......很不错......值得超过只需5 ..
评论会员:克里Diggins 时间:2012/02/06
这是很有种你说,谢谢你! BR}{A9的}