精华内容
下载资源
问答
  • 编译原理抽象语法树 使代码复杂易读和理解的一件事是,方法内部的指令处于不同的抽象级别。 假设我们的应用程序仅允许登录用户查看其朋友的旅行。 如果用户不是朋友,则不会显示任何行程。 一个例子: public ...

    编译原理抽象语法树

    平衡抽象原则 使代码复杂易读和理解的一件事是,方法内部的指令处于不同的抽象级别。

    假设我们的应用程序仅允许登录用户查看其朋友的旅行。 如果用户不是朋友,则不会显示任何行程。

    一个例子:

    public List<Trip> tripsByFriend(User user, User loggedInUser) {
        return (user.friends().contains(loggedInUser))    
                        ? userRepository.findTripsBy(user.id())
                        : Collections.emptyList();
    }

    在上面的代码中,方法主体中的所有指令处于不同的抽象级别。 我们有验证友谊的指令,通过协作者获取朋友的旅行列表的指令以及返回空且不变的列表的低级Java API。 最重要的是,我们拥有商业行为本身。

    现在让我们看一下相同方法的重构版本:

    public List<Trip> tripsByFriend(User user, User loggedInUser) {
        return (user.isFriendsWith(loggedInUser)) 
                        ? tripsBy(user)
                        : noTrips();
    }
    
    private List<Trip> tripsBy(User user) {
        userRepository.findTripsBy(friend.id());
    }
    
    private List<Trip> noTrips() {
        return Collections.emptyList();
    }

    在这个新版本中,我们将低级抽象提取到私有方法中,并且还将某些行为移至User类。 进行此更改后,所有指令都处于相同的抽象级别,从而使业务规则清晰明了。 现在,公共方法可以告诉我们一个故事,而无需担心技术实施细节。 现在,代码读取时没有任何颠簸:“如果用户是已登录用户的朋友,则按用户返回行程,否则不返回行程。”

    平衡抽象原理(BAP)

    平衡抽象原理定义了按较高级别构造分组的所有代码构造应处于同一抽象级别。 这意味着:

    • 方法中的所有指令应处于相同的抽象级别
    • 类内的所有公共方法都应处于相同的抽象级别
    • 包/命名空间中的所有类
    • 父包/命名空间中的所有同级包/命名空间
    • 所有模块,子系统等

    该原理也适用于测试-单个单元(方法,类,模块,系统)的所有测试应处于相同的抽象级别。

    BAP和SRP

    符合“单一职责原则”的代码也更有可能也符合“平衡抽象原则”。 但是,情况并非总是如此,相反的情况并非总是如此。

    结论

    为了获得精心设计的代码,我们需要考虑许多设计原则,我认为,平衡抽象原则(BAP)是SOLID原则和整个软件设计中缺少的部分。

    翻译自: https://www.javacodegeeks.com/2015/03/balanced-abstraction-principle.html

    编译原理抽象语法树

    展开全文
  • 编译原理c语言的抽象语法树 在本文中,我们将看到如何处理和转换从解析器获得的信息。 ANTLR解析器识别源代码中存在的元素,并构建一个解析树 。 从语法分析树中,我们将获得抽象语法树 ,该语法树将用于执行验证并...

    编译原理c语言的抽象语法树

    在本文中,我们将看到如何处理和转换从解析器获得的信息。 ANTLR解析器识别源代码中存在的元素,并构建一个解析树 。 从语法分析树中,我们将获得抽象语法树 ,该语法树将用于执行验证并生成已编译的代码。

    请注意,术语可能会有所不同:许多人会将从ANTLR获得的树称为抽象语法树。 我更喜欢标记这两个步骤的区别。 对我而言, 解析树是对解析器有意义的信息, 抽象语法树是经过重组以更好地支持后续步骤的信息。

    建立自己的语言的系列

    以前的帖子:

    1. 建立一个词法分析器
    2. 建立一个解析器
    3. 创建带有语法突出显示的编辑器
    4. 使用自动补全功能构建编辑器

    代码在GitHub上的标签为05_ ast

    丰富我们的语言

    在本系列文章中,我们一直在研究一种非常简单的表达语言。 现在是时候让我们的语言稍微复杂一些了:

    • 例如, 强制转换为: 10作为十进制(1 * 2.45)作为Int
    • 打印声明   例如: print(3 + 11)

    为此,我们需要修改我们的词法分析器和解析器语法。 我们在之前的文章中构建的语法高亮和自动完成功能将继续起作用。

    新的词法分析器语法:

    lexer grammar SandyLexer;
     
    // Whitespace
    NEWLINE            : '\r\n' | 'r' | '\n' ;
    WS                 : [\t ]+ -> skip ;
     
    // Keywords
    VAR                : 'var' ;
    PRINT              : 'print';
    AS                 : 'as';
    INT                : 'Int';
    DECIMAL            : 'Decimal';
     
    // Literals
    INTLIT             : '0'|[1-9][0-9]* ;
    DECLIT             : '0'|[1-9][0-9]* '.' [0-9]+ ;
     
    // Operators
    PLUS               : '+' ;
    MINUS              : '-' ;
    ASTERISK           : '*' ;
    DIVISION           : '/' ;
    ASSIGN             : '=' ;
    LPAREN             : '(' ;
    RPAREN             : ')' ;
     
    // Identifiers
    ID                 : [_]*[a-z][A-Za-z0-9_]* ;

    以及新的解析器语法:

    parser grammar SandyParser;
     
    options { tokenVocab=SandyLexer; }
     
    sandyFile : lines=line+ ;
     
    line      : statement (NEWLINE | EOF) ;
     
    statement : varDeclaration # varDeclarationStatement
              | assignment     # assignmentStatement
              | print          # printStatement ;
     
    print : PRINT LPAREN expression RPAREN ;
     
    varDeclaration : VAR assignment ;
     
    assignment : ID ASSIGN expression ;
     
    expression : left=expression operator=(DIVISION|ASTERISK) right=expression # binaryOperation
               | left=expression operator=(PLUS|MINUS) right=expression        # binaryOperation
               | value=expression AS targetType=type                           # typeConversion
               | LPAREN expression RPAREN                                      # parenExpression
               | ID                                                            # varReference
               | MINUS expression                                              # minusExpression
               | INTLIT                                                        # intLiteral
               | DECLIT                                                        # decimalLiteral ;
     
    type : INT     # integer
         | DECIMAL # decimal ;

    抽象语法树元模型

    抽象语法树元模型只是我们要用于抽象语法树(AST)的数据结构。 在这种情况下,我们通过定义将用于AST的类来定义它。

    AST元模型看起来与解析树元模型相当类似,即ANTLR生成的包含节点的类集。

    将有一些主要区别:

    • 它会比ANTLR生成的类(因此构成解析树的类)具有更简单,更好的API。 在下一节中,我们将看到此API如何允许在AST上执行转换
    • 我们将删除仅在解析时才有意义但在逻辑上是无用的元素:例如括号表达式或行节点
    • 我们在解析树中具有单独实例的某些节点可以对应于AST中的单个实例。 这是类型引用IntDecimal的情况,它们在AST中使用单例对象定义
    • 我们可以为相关节点类型(例如BinaryExpression)定义通用接口
    • 为了定义如何解析变量声明,我们重用分配规则。 在AST中,这两个概念是完全分开的
    • 某些操作在解析树中具有相同的节点类型,但在AST中是分开的。 不同类型的二进制表达式就是这种情况

    让我们看看如何使用Kotlin定义AST元模型。

    interface Node
     
    //
    // Sandy specific part
    //
     
    data class SandyFile(val statements : List) : Node
     
    interface Statement : Node { }
     
    interface Expression : Node { }
     
    interface Type : Node { }
     
    //
    // Types
    //
     
    object IntType : Type
     
    object DecimalType : Type
     
    //
    // Expressions
    //
     
    interface BinaryExpression : Expression {
        val left: Expression
        val right: Expression
    }
     
    data class SumExpression(override val left: Expression, override val right: Expression) : BinaryExpression
     
    data class SubtractionExpression(override val left: Expression, override val right: Expression) : BinaryExpression
     
    data class MultiplicationExpression(override val left: Expression, override val right: Expression) : BinaryExpression
     
    data class DivisionExpression(override val left: Expression, override val right: Expression) : BinaryExpression
     
    data class UnaryMinusExpression(val value: Expression) : Expression
     
    data class TypeConversion(val value: Expression, val targetType: Type) : Expression
     
    data class VarReference(val varName: String) : Expression
     
    data class IntLit(val value: String) : Expression
     
    data class DecLit(val value: String) : Expression
     
    //
    // Statements
    //
     
    data class VarDeclaration(val varName: String, val value: Expression) : Statement
     
    data class Assignment(val varName: String, val value: Expression) : Statement
     
    data class Print(val value: Expression) : Statement

    我们首先定义Node 。 节点代表AST的每个可能节点,它是通用的。 它也可以重用于其他语言。 其余所有语言都是特定于语言的(本例中为Sandy)。 在我们的特定语言中,我们需要三个重要的界面:

    • 声明
    • 表达
    • 类型

    这些接口均扩展Node

    然后,我们声明我们在语言中使用的两种类型。 它们被定义为单例对象。 这意味着我们只有这些类的一个实例。

    然后我们有了BinaryExpression接口该接口扩展了Expression 。 对于类实现它,每个基本算术表达式一个。

    大多数表达式具有其他节点作为子节点。 有些具有简单的值。 他们是VarReference(其中有一个String类型的属性varName中 ),以及Intlit和DecLit(都有一个String类型的属性 )。

    最后,我们有三个实现Statement的类。

    请注意,我们正在使用数据类,因此我们可以免费使用hashCode,equals和toString方法。 Kotlin还为我们生成了构造函数和获取方法。 试想一下,用Java编写多少代码。

    将解析树映射到抽象语法树

    让我们看看如何获​​取由ANTLR生成的解析树,并将其映射到我们的AST类中。

    fun SandyFileContext.toAst() : SandyFile = SandyFile(this.line().map { it.statement().toAst() })
     
    fun StatementContext.toAst() : Statement = when (this) {
        is VarDeclarationStatementContext -> VarDeclaration(varDeclaration().assignment().ID().text, varDeclaration().assignment().expression().toAst())
        is AssignmentStatementContext -> Assignment(assignment().ID().text, assignment().expression().toAst())
        is PrintStatementContext -> Print(print().expression().toAst())
        else -> throw UnsupportedOperationException(this.javaClass.canonicalName)
    }
     
    fun  ExpressionContext.toAst() : Expression = when (this) {
        is BinaryOperationContext -> toAst()
        is IntLiteralContext -> IntLit(text)
        is DecimalLiteralContext -> DecLit(text)
        is ParenExpressionContext -> expression().toAst()
        is VarReferenceContext -> VarReference(text)
        is TypeConversionContext -> TypeConversion(expression().toAst(), targetType.toAst())
        else -> throw UnsupportedOperationException(this.javaClass.canonicalName)
    }
     
    fun TypeContext.toAst() : Type = when (this) {
        is IntegerContext -> IntType
        is DecimalContext -> DecimalType
        else -> throw UnsupportedOperationException(this.javaClass.canonicalName)
    }
     
    fun  BinaryOperationContext.toAst() : Expression = when (operator.text) {
        "+" -> SumExpression(left.toAst(), right.toAst())
        "-" -> SubtractionExpression(left.toAst(), right.toAst())
        "*" -> MultiplicationExpression(left.toAst(), right.toAst())
        "/" -> DivisionExpression(left.toAst(), right.toAst())
        else -> throw UnsupportedOperationException(this.javaClass.canonicalName)
    }

    为了实现这一点,我们利用了Kotlin的三个非常有用的功能:

    • 扩展方法:我们将方法toAst添加到了几个现有的类中
    • when构造,它是switch的更强大的版本
    • 智能转换:在检查对象是否具有某个类之后,编译器会将其隐式转换为该类型,以便我们可以使用该类的特定方法

    我们可以提出一种机制,自动为大多数规则导出此映射,并仅在分析树和AST不同的地方对其进行自定义。 为了避免使用过多的反射黑魔法,我们暂时不这样做。 如果我使用Java,那么我会走上一条反思之路,以避免不得不手动编写大量多余和无聊的代码。 但是,使用Kotlin可以使这段代码紧凑而清晰。

    测试映射

    当然,我们需要测试这些东西。 让我们看看对于某段代码获得的AST是否符合我们的期望。

    class MappingTest {
     
        @test fun mapSimpleFile() {
            val code = """var a = 1 + 2
                         |a = 7 * (2 / 3)""".trimMargin("|")
            val ast = SandyParserFacade.parse(code).root!!.toAst()
            val expectedAst = SandyFile(listOf(
                    VarDeclaration("a", SumExpression(IntLit("1"), IntLit("2"))),
                    Assignment("a", MultiplicationExpression(
                            IntLit("7"),
                            DivisionExpression(
                                    IntLit("2"),
                                    IntLit("3"))))))
            assertEquals(expectedAst, ast)
        }
     
        @test fun mapCastInt() {
            val code = "a = 7 as Int"
            val ast = SandyParserFacade.parse(code).root!!.toAst()
            val expectedAst = SandyFile(listOf(Assignment("a", TypeConversion(IntLit("7"), IntType))))
            assertEquals(expectedAst, ast)
        }
     
        @test fun mapCastDecimal() {
            val code = "a = 7 as Decimal"
            val ast = SandyParserFacade.parse(code).root!!.toAst()
            val expectedAst = SandyFile(listOf(Assignment("a", TypeConversion(IntLit("7"), DecimalType))))
            assertEquals(expectedAst, ast)
        }
     
        @test fun mapPrint() {
            val code = "print(a)"
            val ast = SandyParserFacade.parse(code).root!!.toAst()
            val expectedAst = SandyFile(listOf(Print(VarReference("a"))))
            assertEquals(expectedAst, ast)
        }
     
    }

    那就太好了:我们有了代码中存在的信息的清晰模型。 元模型和映射代码看起来非常简单清晰。 但是,我们需要添加一些细节:节点在源代码中的位置。 向用户显示错误时将需要这样做。 我们希望有可能指定AST节点的位置,但我们不想被迫这样做。 这样,根据我们需要执行的操作,我们可以忽略或不忽略这些头寸。 考虑一下我们到目前为止编写的测试:是否必须为所有节点指定虚假位置会很麻烦又烦人? 我认同。

    这是新的Node定义和一些支持类:

    interface Node {
        val position: Position?
    }
     
    data class Point(val line: Int, val column: Int)
     
    data class Position(val start: Point, val end: Point)
     
    fun pos(startLine:Int, startCol:Int, endLine:Int, endCol:Int) = Position(Point(startLine,startCol),Point(endLine,endCol))

    我们还需要将位置作为可选参数添加到所有类中。 它将具有默认值null 。 例如,这是SandyFile现在的外观:

    data class SandyFile(val statements : List<Statement>, override val position: Position? = null) : Node

    映射只是稍微复杂一点:

    fun SandyFileContext.toAst(considerPosition: Boolean = false) : SandyFile = SandyFile(this.line().map { it.statement().toAst(considerPosition) }, toPosition(considerPosition))
     
    fun Token.startPoint() = Point(line, charPositionInLine)
     
    fun Token.endPoint() = Point(line, charPositionInLine + text.length)
     
    fun ParserRuleContext.toPosition(considerPosition: Boolean) : Position? {
        return if (considerPosition) Position(start.startPoint(), stop.endPoint()) else null
    }
     
    fun StatementContext.toAst(considerPosition: Boolean = false) : Statement = when (this) {
        is VarDeclarationStatementContext -> VarDeclaration(varDeclaration().assignment().ID().text, varDeclaration().assignment().expression().toAst(considerPosition), toPosition(considerPosition))
        is AssignmentStatementContext -> Assignment(assignment().ID().text, assignment().expression().toAst(considerPosition), toPosition(considerPosition))
        is PrintStatementContext -> Print(print().expression().toAst(considerPosition), toPosition(considerPosition))
        else -> throw UnsupportedOperationException(this.javaClass.canonicalName)
    }
     
    fun  ExpressionContext.toAst(considerPosition: Boolean = false) : Expression = when (this) {
        is BinaryOperationContext -> toAst(considerPosition)
        is IntLiteralContext -> IntLit(text, toPosition(considerPosition))
        is DecimalLiteralContext -> DecLit(text, toPosition(considerPosition))
        is ParenExpressionContext -> expression().toAst(considerPosition)
        is VarReferenceContext -> VarReference(text, toPosition(considerPosition))
        is TypeConversionContext -> TypeConversion(expression().toAst(considerPosition), targetType.toAst(considerPosition), toPosition(considerPosition))
        else -> throw UnsupportedOperationException(this.javaClass.canonicalName)
    }
     
    fun TypeContext.toAst(considerPosition: Boolean = false) : Type = when (this) {
        is IntegerContext -> IntType(toPosition(considerPosition))
        is DecimalContext -> DecimalType(toPosition(considerPosition))
        else -> throw UnsupportedOperationException(this.javaClass.canonicalName)
    }
     
    fun  BinaryOperationContext.toAst(considerPosition: Boolean = false) : Expression = when (operator.text) {
        "+" -> SumExpression(left.toAst(considerPosition), right.toAst(considerPosition), toPosition(considerPosition))
        "-" -> SubtractionExpression(left.toAst(considerPosition), right.toAst(considerPosition), toPosition(considerPosition))
        "*" -> MultiplicationExpression(left.toAst(considerPosition), right.toAst(considerPosition), toPosition(considerPosition))
        "/" -> DivisionExpression(left.toAst(considerPosition), right.toAst(considerPosition), toPosition(considerPosition))
        else -> throw UnsupportedOperationException(this.javaClass.canonicalName)
    }

    在这一点上,所有以前的测试都一直通过,但是我们想添加一个测试来验证位置是否正确定义:

    @test fun mapSimpleFileWithPositions() {
            val code = """var a = 1 + 2
                         |a = 7 * (2 / 3)""".trimMargin("|")
            val ast = SandyParserFacade.parse(code).root!!.toAst(considerPosition = true)
            val expectedAst = SandyFile(listOf(
                    VarDeclaration("a",
                            SumExpression(
                                    IntLit("1", pos(1,8,1,9)),
                                    IntLit("2", pos(1,12,1,13)),
                                    pos(1,8,1,13)),
                            pos(1,0,1,13)),
                    Assignment("a",
                            MultiplicationExpression(
                                IntLit("7", pos(2,4,2,5)),
                                DivisionExpression(
                                        IntLit("2", pos(2,9,2,10)),
                                        IntLit("3", pos(2,13,2,14)),
                                        pos(2,9,2,14)),
                                pos(2,4,2,15)),
                            pos(2,0,2,15))),
                    pos(1,0,2,15))
            assertEquals(expectedAst, ast)
        }

    解析树包含以最方便的方式为解析器组织的信息。 通常,这不是执行以下步骤的最方便的方法。 考虑一下通过重用赋值规则来实现的变量声明规则:当然,这会使语法更短,并且对解析树有意义。 但是,从逻辑角度看,这两个元素是分开的,并且在AST中它们确实是分开的。

    我们其余的大多数工具都可以在AST上运行,因此最好花一些时间在有意义的AST上工作。

    参考: 为您自己的语言构建编译器:从 Federico Tomassetti博客上的JCG合作伙伴 Federico Tomassetti 的解析树到抽象语法树

    翻译自: https://www.javacodegeeks.com/2016/08/building-compiler-language-parse-tree-abstract-syntax-tree.html

    编译原理c语言的抽象语法树

    展开全文
  •  构造抽象语法树构造基于DFA的正则表达式引擎的第一步。目前在我实现的这个正则表达式的雏形中,正则表达式的运算符有3种,表示选择的|运算符,表示星号运算的*运算符,表示连接的运算符cat(在实际正则表达式中...

    简要介绍

        构造抽象语法树是构造基于DFA的正则表达式引擎的第一步。目前在我实现的这个正则表达式的雏形中,正则表达式的运算符有3种,表示选择的|运算符,表示星号运算的*运算符,表示连接的运算符cat(在实际正则表达式中被省去)。

    例如对于正则表达式a*b|c,在a*和b之间省略了连接运算符cat。其中|、cat运算符是双目运算符,*运算符是单目运算符。


    下图来自编译原理一书:


    对(a|b)*abb构造语法树,需要注意的是,此图中在原正则表达式的末尾添加了一个#号表示接受状态。在我自己的代码中没有添加最后一个#号,而是用 eType_END 类型的词法单元表示正则表达式的末尾和DFA的接受状态。


    构造正则表达式的抽象语法树的过程和构造算术表达式的抽象语法树的过程类似,都一样会存在运算符优先级和括号处理的问题。有差异的地方是正则表达式中存在单目运算符*,而普通的算术表达式中都是双目运算符。


    构造正则表达式语法树的过程基于词法分析,这里的词法分析就比较简单了,因为一个字符就对应一个词法单元,需要注意的地方是:

    1 在两个非运算符、右括号左括号对之间需要添加cat连接运算符。

    2 在尾部需要加入一个 eType_END 类型的词法单元表示正则表达式的末尾和DFA的接受状态。


    语法树展示

    根据正则表达式得到的语法树如下所示:


    支持转义字符(右斜杠\)的模式串:


    在我写的词法分析中支持通配符点号(.),支持转义字符(右斜杠\加特殊字符)。另外这个语法分析树的打印方式大家也可以从我的代码中找到实现方法^_^。

    在以上各个语法树中,打印输出时屏蔽了尾部的eType_END节点。


    构建语法树主要需要的对象和数据结构

    构建语法树主要需要的对象和数据结构如下:


    整个语法树的构建过程中需要一个词法分析器Lex,词法分析器从左到右逐个字符地扫描正则表达式,根据遇到的字符返回正确的Token给语法树构建器,对于不合法的正则表达式给出报错信息(例如转义字符\后面跟的不是特殊字符)。

    语法树构建器拿到词法分析器返回的词法Token后,开始进行自下而上的建树过程,在不考虑括号的情况下,正确的正则表达式的第一个词法Token应该是一个非运算符,它被包装为语法树节点结构然后被压入语法树构建器的语法树节点栈中。

    之后第二个词法Token可能是一个运算符也可能是一个非运算符,如果是非运算符,则需要添加一个表示连接的cat运算符到运算符栈中,并将得到的操作数Token包装为语法树节点压入语法树节点栈中。每次向运算符栈中压入新的运算符new之前,都需要查看当前运算符栈顶的运算符old,和new谁的优先级更高,如果old的优先级较高,则先处理old运算符(会用掉语法树节点栈中的节点,运算得到的节点再压回语法树节点栈),old被处理完后,old出栈,接下来的栈顶元素成为old,再次和new进行比较,重复这个过程,直到old的运算符优先级低于new,再将new运算符压栈。如果遇到了左括号,则先将左括号压入运算符栈中,在遇到右括号时需要将运算符栈中的节点从栈顶开始处理,直到处理到最靠近栈顶的左括号为止。

    当正则表达式处理完后,最后再处理运算符栈中剩余的运算符。

    正确的结果应该是运算符栈为空,语法树节点栈中有一个节点,这个节点就是整个语法树的根节点。


    结合实例介绍构建语法树过程

    接下来举一个实例,对正则表达式(a|b)*a|bcd 构造语法树。过程如下:
    1 词法分析器从左向右扫描表达式,先得到左括号,将左括号包装成节点,压入运算符栈中;
    2 词法分析器获得的下一个节点为字符a,压入语法树节点栈中;
    3 词法分析器继续获取词法Token,得到运算符|,压入运算符栈中;
    4 下一个字符是b,将b包装成节点压入语法树节点栈中;
    5 继续获取字符,得到右括号,此时语法树构建器开始根据语法树节点栈和运算符栈进行运算合并已有节点,直到在语法树节点栈中遇到左括号为止。开始处理时语法树节点栈和运算符栈中内容如下:
    运算符栈:(|
    语法树节点栈:ab
    运算符栈的栈顶运算符出栈,得到|运算符,这是一个双目运算符,所以从语法树节点栈中出栈2个节点b和a,|运算符和节点a节点b,得到新的节点(记为M),M再压入语法树节点栈,此时在运算符栈顶已经是左括号,将其出栈,节点合并结束。
    两个栈的内容如下:
    运算符栈:空
    语法树节点栈:M
    6 接下来是*号运算符,因为*号是优先级最高的运算符,所以可以直接处理,无需进行运算符优先级的比较,*号会消耗语法树节点栈中一个节点(也就是M),*号运算符和M节点运算得到新的节点N,重新压入节点栈中。

    7 接下来词法分析器得到字符a,但是在节点N和字符a之间需要插入一个连接cat运算符,我们把cat运算符用‘+’来表示,‘+’压入运算符栈,a压入节点栈。
    8 词法分析器得到的下一个Token是运算符|,在向运算符栈中压入‘|’运算符之前,我们需要检查运算符栈的栈顶运算符和当前想要压栈的运算符的优先级,如果栈顶运算符的优先级大于等于将要压栈的运算符,则需要先处理栈顶的运算符(这里是一个循环的过程,也就是说处理完栈顶的运算符之后,还要继续比较栈顶的运算符和将要压栈的运算符之间的优先级,以决定接下来该执行什么步骤)。在这里栈顶的运算符‘+’的优先级比运算符‘|’的优先级高,所以先进行栈顶运算符的运算,‘+’连接运算符将节点N和a组成为新的节点(记为P)并重新压入节点栈中。然后运算符栈为空,此时把前面所说的“将要压入运算符栈的‘|’运算符”压入运算符栈。
    9 下一个字符是b,此时不需要插入连接运算符,只需要将字符b包装为节点压入节点栈。
    10 下一个字符是c,此时同样需要插入一个连接运算符,在向运算符栈中压入‘+’运算符之前,我们需要检查运算符栈的栈顶运算符和当前想要压栈的运算符的优先级。在这里‘+’的优先级高于栈顶的‘|’,所以直接将运算符‘+’压入运算符栈中,并将字符c包装为节点压入节点栈。
    11 下一个字符是d,此时同样需要插入一个连接运算符,在向运算符栈中压入‘+’运算符之前,我们需要检查运算符栈的栈顶运算符和当前想要压栈的运算符的优先级。在这里两个运算符相同,所以先处理运算符栈栈顶的运算符,‘+’运算符和节点栈中的b,c字符组成新的节点Q压入节点栈,然后运算符栈顶的运算符为‘|’,‘+’的优先级高于‘|’,所以不在处理运算符栈的栈顶运算符。将‘+’压入运算符栈,将字符d包装为节点压入节点栈。
    12 此时词法分析器报告已经到达正则表达式的结尾,所以开始处理运算符栈中剩余的运算符,从栈顶开始依次处理,首先遇到的是‘+’连接符,从节点栈中取出节点Q和字符d生成新的节点R压回节点栈。
    13 继续处理运算符栈,栈顶运算符为‘|’,从节点栈中取出节点P和节点R生成新的节点S压回节点栈。
    14 此时运算符栈清空,节点栈中只有一个节点S,S就是最终生成的语法树的根节点。(至此大功告成、功德圆满^_^呼呼)
    可以看出,我们遇到一个非运算符时,需要检查是否需要添加cat连接符,在向运算符栈中添加一个新的运算符时,需要比较栈顶运算符和将要添加的运算符之间的优先级,以决定是否先进行栈顶运算符的运算。

    我们将上面每一个步骤中的运算符栈和节点栈以图形的方式直观地展现出来:


    展开全文
  • sql 解析,编译,ast 抽象语法树

    万次阅读 2018-03-06 01:35:35
    `),中文是抽象语法树。在不说子查询之类的情况下,这个AST也不会太复杂,毕竟where后面的情况比起编译原理里的程序语言来说情况还是要少得多的。以上述的sql为例,这里解析出来的Where结构是这样的: & sqlparser ....

    Day4: 《将sql转换为es的DSL》

    es现在几乎已经是开源搜索引擎的事实标准了,搭建简易,使用方便。不过在很多公司里(包括我司的部分部门),并不是把它当搜索引擎来用,而是当db来用。因为本身查询/搜索原理的区别,使es在千万或者亿级的数据中进行逻辑筛选相对高效。例如一些wms、工单查询系统,单表几十个甚至上百个字段,如果在数据库里为每种类型的查询都建立合适的索引,成本比较高,更不用说索引建多了还会影响到插入速度,后期的索引优化也是比较麻烦的问题。

    不过如果把es当db来使的话,始终会有一个绕不过去的坎。就是es的DSL。让所有业务开发去学习dsl的话也不是不可以,但DSL真的有点反人类(不要打我)。简单的a and b或者a or b还比较容易写,如果我要的是a and (b and (c or d) and e)的查询逻辑,那我觉得谁写都会晕。即使是用官方或者第三方提供的client,如果需求多种多样的话,想要灵活地实现`需求=>DSL`的过程还是比较痛苦。

    对于业务开发来说,当然是sql更平易近人(毕竟写了这么多年CRUD)。所以还有一种歪门邪道的流派,直接把sql转成DSL。要做sql和DSL转换的工作,需要进行sql的解析,先不要怵,这个年代找一个靠谱的sql parser还是比较容易的。比如阿里开源的druid连接池里的sql模块:
     
    https://github.com/alibaba/dru ... d/sql

    因为笔者的实现是用的下面这个golang版的parser:

    https://github.com/xwb1989/sqlparser

    所以用这个来举例吧~

    这个是其作者从youtube/vitness里提取并进行改进的一个parser,我们能用到的是一部分子集功能,只需要解析select类的sql。

    先举个简单的sql的例子:
    select * from x_order where userId = 1 order by id desc limit 10,1;
    
    解析之后会变成golang的一个struct,来看看具体的定义:
    
    &sqlparser.Select{
        Comments:sqlparser.Comments(nil),
        Distinct:"",
        SelectExprs:sqlparser.SelectExprs{(*sqlparser.StarExpr)(0xc42000aee0)},
        From:sqlparser.TableExprs{(*sqlparser.AliasedTableExpr)(0xc420016930)},
        Where:(*sqlparser.Where)(0xc42000afa0),
        GroupBy:sqlparser.GroupBy(nil),
        Having:(*sqlparser.Where)(nil),
        OrderBy:sqlparser.OrderBy{(*sqlparser.Order)(0xc42000af20)},
        Limit:(*sqlparser.Limit)(0xc42000af80),
        Lock:""
    }


    sql的select语句在被解析之后生成一个Select的结构体,如果我们不关心使用者需要的字段的话,可以先把SelectExprs/Distinct/Comments/Lock里的内容忽略掉。如果不是分组统计类的需求,也可以先把GroupBy/Having忽略掉。这里我们关心的就剩下From、Where、OrderBy和Limit。

    From对应的TableExprs实际上可以认为是简单的字符串,这里的值其实就是`x_order`。

    OrderBy实际上是一个元素为
    type Order struct {
        Expr      ValExpr
        Direction string
    }\

    的数组。

    Limit也很简单,
    type Limit struct {
        Offset, Rowcount ValExpr
    }

    其实就是俩数字。

    那么剩下的就是这个Where结构了。where会被解析为AST(` https://en.wikipedia.org/wiki/Abstract_syntax_tree`),中文是抽象语法树。在不说子查询之类的情况下,这个AST也不会太复杂,毕竟where后面的情况比起编译原理里的程序语言来说情况还是要少得多的。以上述的sql为例,这里解析出来的Where结构是这样的:
    &sqlparser.Where{
        Type:"where",
        Expr:(*sqlparser.ComparisonExpr)(0xc420016a50)
    }


    只有一个节点,一个ComparisonExpr表达式,这个ComparisonExpr,中文比较表达式,指代的就是我们sql里的`user_id = 1`。实际上我们可以认为这个"比较表达式"即是所有复杂AST的叶子节点。叶子结点在AST遍历的时候一般也就是递归的终点。因为这里的查询比较简单,整棵AST只有一个节点,即根节点和叶子节点都是这个ComparisonExpr。

    再来一个复杂点的例子。
    select * from users where user_id = 1 and product_id =2
    
    =>
    
    &sqlparser.Where{
        Type:"where",
        Expr:(*sqlparser.AndExpr)(0xc42000b020)
    }
    
    AndExpr有LeftRight两个成员,分别是:
    
    Left:
    &sqlparser.ComparisonExpr{
        Operator:"=",
        Left:(*sqlparser.ColName)(0xc4200709c0),
        Right:sqlparser.NumVal{0x31}
    }
    
    Right:
    &sqlparser.ComparisonExpr{
        Operator:"=",
        Left:(*sqlparser.ColName)(0xc420070a50),
        Right:sqlparser.NumVal{0x32}
    }


    稍微有一些二叉树的样子了吧。把这棵简单的树画出来:


    Untitled1.png



    回到文章开头的那个复杂的例子:
    a and (b and (c or d) and e)
    
    =>
    
    select * from user_history where user_id = 1 and (product_id = 2 and (star_num = 4 or star_num = 5) and banned = 1)
    


    看着真够麻烦的,我们把这棵树画出来:


    Untitled.png



    这样看着就直观多了。我们有了AST的结构,那要怎么对应到es的查询DSL呢?少安毋躁。

    我们知道es的bool query是可以进行嵌套的,所以实际上我们可以同样可以构造出树形结构的bool query。这里把bool嵌套must和bool嵌套should简化一下,写成boolmust和boolshould:

    例如a and (b and c)
    query {
        boolmust {
            a,
            boolmust {
                b,
                c
            }
        }
    }


    我们把query内部的第一个boolmust当作根节点,内部嵌套的a和另一个boolmust当作它的两个子节点,然后b和c又是这个boolmust的子节点。可以看出来,实际上这棵树和AST的节点可以一一对应。

    再回到文章开头的例子,a and (b and (c or d) and e):
    query {
        boolmust {
            a,
            boolmust {
                b,
                boolshould {
                    c,
                    d
                },
                e
            }
        }
    }

    和前文中ast来做个简单的结构对比~


    dsl和ast对比.png




    和前文中sql的where解析后的AST树也是完全匹配的。思路来了,只要对sql解析生成的AST进行递归,即可得到这棵树。当然了,这里还可以进行一些优化,如果子节点的类型和父
    节点的类型一致,例如都是and表达式或者都是or表达式,我们可以在生成dsl的时候将其作为并列的节点进行合并,这里不再赘述。


    在递归中有这么几种情况:
    AndExpr => bool must [{left}, {right}]
    OrExpr => bool should [{left}, {right}]
    ComparisonExpr => 一般是叶子节点
    ParenBoolExpr => 指代括号表达式,其实内部是上述三种节点的某一种,所以直接取出内部节点按上述方法来处理


    这样问题就变成了如何处理AST的叶子节点。前面提到了叶子节点实际上就是Comparison Expression。只要简单进行一些对应即可,下面是我们的项目里的一些对应关系,仅供参考:


    convert.png

    最后再附上demo
     
    https://github.com/cch123/elasticsql
    展开全文
  • 编译原理(计算机专业课程)编辑锁定讨论上传视频编译原理是计算机专业的一门重要专业课,旨在介绍编译程序构造的一般原理和基本方法。内容包括语言和文法、词法分析、语法分析、语法制导翻译、中间代码生成、存储管理...
  • 构造抽象语法树构造基于DFA的正则表达式引擎的第一步。目前在我实现的这个正则表达式的雏形中,正则表达式的运算符有3种,表示选择的|运算符,表示星号运算的*运算符,表示连接的运算符cat(在实际正则表达式中被...
  • https://gitee.com/wukangio/helison.archer 参上!
  • 我们由此可以画出他的抽象语法树,如下: 那么,abp为此句型的短语总结来说:一个句型的语法树中任一子树叶结点所组成的符号串都是该句型的短语,由这概念,那么我们自然可以想到,b也应该是该...
  • 为了将某个串的推导表示得更为清楚,我们需要表示出终结符串的结构,这通常是被表示为树结构,它称为分析。 转载于:https://www.cnblogs.com/cwblaze/archive/2010/02/19/1669475.html...
  • 原文:JavaScript的工作原理:解析、抽象语法树(AST)+ 提升编译速度5个技巧 作者:前端小智 Fundebug经授权转载,版权归原作者所有。 这是专门探索 JavaScript 及其所构建的组件的系列文章的第 14 篇。 如果你...
  • AST抽象语法树

    千次阅读 2018-09-16 13:54:23
    抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套...
  • 编译原理手记06-语法树

    千次阅读 2014-07-24 16:19:50
    语法树是一种用树形数据结构来描述
  • AST 抽象语法树学习

    万次阅读 2017-05-20 21:01:09
    阅读原文Abstract Syntax Tree 抽象语法树简介在使用前端许多工具插件的时候,我们大多知道每个工具库、每个插件能做什么,不过很多同学其实并不清楚背后用到的技术,如webpack、rollup、UglifyJS、Lint等很多的工具...
  • 抽象语法树

    2021-04-24 21:08:54
    一、什么是AST抽象语法树 在传统的编译语言的流程中,程序的一段源代码在执行之前会经历三个步骤,统称为"编译": 分词/词法分析 这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块统称为词法单元...
  • AST 抽象语法树

    2019-02-14 16:19:00
    在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。 ...
  • 抽象语法树-AST

    2019-04-28 14:55:07
    抽象语法树(Abstract Syntax Tree) ...这些工具的原理都是通过JavaScript Parser把代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构,通过操纵这颗树,我们可以精准的定位到声明语句、赋值语句、运算语句...
  • 抽象语法树简介(一)简介抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一...
  • Java抽象语法树AST浅析与使用

    万次阅读 2019-10-30 10:51:02
    抽象语法树(Abstract Syntax Tree, AST)是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的结构,树的每个节点ASTNode都表示源码中的一个结构。Eclipse java的开发工具(JDT)提供了Java源代码的抽象...
  • 一、概念介绍: 在开发Vue的时候编译器会...AST抽象语法树服务于模板编译,将一种语法翻译为另一种语法。在Vue中将模板语法编译为HTML语法,自己作为中转站。 二、抽象语法树与虚拟DOM节点的关系: 图示: 抽..
  • AST(抽象语法树)超详细

    万次阅读 多人点赞 2019-07-15 17:15:10
    首先来一个比较形象的,转载自:AST-抽象语法树,讲述了为什么需要讲源代码转化为AST,总结就是:AST不依赖于具体的文法,不依赖于语言的细节,我们将源代码转化为AST后,可以对AST做很多的操作,...
  • 1 语法树(parse tree): 是在parsing阶段,derivation的图像化表示,parser tree focus on grammar的actual implemment,包括像white spaces, braces, keywords, parenthesis 等一些细节。 “parse tree” 也叫 ...
  • 通过设计、开发一个高级语言的LL(1)语法分析程序,实现 对源程序的语法检查和结构分析,括自顶向下语法分析、First集、Follow集、Select集、文法等价变换)的理解,提高语法分析方法的实践能力。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 18,870
精华内容 7,548
关键字:

编译原理构造抽象语法树