AST

Workflow

源码 (string)
│ Lexical Analysis (词法分析)

Token 流 (List)
│ Syntactic Analysis (语法分析 / parsing)

Parse Tree / CST
│ (简化、抽象)

AST
│ Semantic Analysis (语义分析: 类型检查、符号表…)

后端 (代码生成 / 或者对你来说: 校验、分析、重写)

调用TSql160Parser.Parse()时,这一整条流水线都会跑一遍。ScriptDom内部先有一个T-SQL lexer把脚本切成token,再由parser建成TSqlFragment树。

Lexer (Tokenizer/Scanner)

Lexer从左到右扫描字符,按规则切成一个一个token,也就是语言的最小有意义单位。每个token通常包含:

C#
record Token(TokenType Type, string Text, int Position);
// 例如: (TokenType.Number, "42", 17)

Lexer只负责分词,parser负责判断句子语法对不对。空白和注释会被lexer舍弃。

Parser

Parser的工作就是按照语言的grammar把线性token序列组织成树。树可以是AST也可以是CST,但这里我们只focus on AST。

AST不是普通的树,也就是所有节点是同一类型的树。AST的节点是几百个不同的类组成的继承体系:ScriptDom里的TSqlFragment里约有1000个子类,每种节点结构还不一样,例如BinaryExpr有左右孩子,IfStatement有条件,then快,else块,Literal没有孩子。

验证AST

两种方式。第一种,也是比较符合直觉的:

C#
void Check(TSqlFragment node)
{
    if (node == null) return;   // 很多孩子字段是可选的, 比如没有 WHERE 就是 null

    switch (node)
    {
        case SelectStatement s:
            // 1. 处理当前节点(你的检查逻辑)
            // 2. 手动递归进【每一个】孩子字段——一个都不能漏
            Check(s.WithCtesAndXmlNamespaces);
            Check(s.QueryExpression);
            foreach (var opt in s.OptimizerHints) Check(opt);
            break;

        case QuerySpecification q:          // SELECT 的主体
            foreach (var sel in q.SelectElements) Check(sel);
            Check(q.FromClause);
            Check(q.WhereClause);
            Check(q.GroupByClause);
            Check(q.HavingClause);
            Check(q.OrderByClause);
            Check(q.TopRowFilter);
            break;

        case WhereClause w:
            Check(w.SearchCondition);
            break;

        case BooleanBinaryExpression b:     // AND / OR
            Check(b.FirstExpression);
            Check(b.SecondExpression);
            break;

        case BooleanComparisonExpression c: // = > < 等
            Check(c.FirstExpression);
            Check(c.SecondExpression);
            break;

        case ScalarSubquery sub:            // 子查询: 里面又是完整的 QueryExpression
            Check(sub.QueryExpression);
            break;

        case ExecuteStatement e:
            ReportViolation(e);             // 叶子级检查, EXEC 下面我们不关心了
            break;

        // ...... ScriptDom 有约 1000 种节点
        // 每种的孩子字段名字、数量、是单个还是 List 全都不一样
        // 这些知识只能靠翻文档/源码, 编译器不会提醒你漏了哪个
    }
}

// 入口就是:
Check(parsedFragment);   // 从根开始, 递归自然铺满整棵树

非常的逆天。第二种,就是用visitor pattern:

C#
// 节点这边: 每个节点类实现 Accept,只干一件事——"报出自己的真实类型"
abstract class Expr { public abstract void Accept(IExprVisitor v); }

class BinaryExpr : Expr
{
    public Expr Left, Right; public string Op;
    public override void Accept(IExprVisitor v) => v.Visit(this); // ← this 是 BinaryExpr
}
class LiteralExpr : Expr
{
    public double Value;
    public override void Accept(IExprVisitor v) => v.Visit(this); // ← this 是 LiteralExpr
}

// 操作这边: 每种操作是一个 visitor 类,每种节点类型一个 Visit 重载
interface IExprVisitor
{
    void Visit(BinaryExpr node);
    void Visit(LiteralExpr node);
}