news 2026/9/25 3:26:47

Buck 的 PSI 文法参考:Python 2.7 完整 BNF 文法及其在 ideabuck 解析器中的落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Buck 的 PSI 文法参考:Python 2.7 完整 BNF 文法及其在 ideabuck 解析器中的落地
  • 开发工具
  • 构建工具

【免费下载链接】buck

A fast build system that encourages the creation of small, reusable modules over a variety of platforms and languages.

项目地址:https://gitcode.com/gh_mirrors/bu/buck
点击查看免费下载

本文以 Buck 仓库 tools/psi-grammar/docs/Python2Grammar.md 为骨架,完整解读 CPython 2.7 的正式文法(BNF 产生式全集):从三个起始符号、语句与复合语句结构,到表达式的 12 级优先级层级、推导式与testlist_safe的历史细节;同时结合仓库中 NextGenIdeabuckGrammar.bnf 与 Buck.bnf 的逐行注释,说明 Buck 的 ideabuck 插件是如何把这份 Python 文法裁剪成 Starlark 子集、并经 JFlex + Grammar-Kit 两遍生成变成可运行的 IntelliJ PSI 解析器的。读完后你既能独立读懂任意一份 BNF 文法,也能理解一个 IDE 语法插件从文法文件到词法/语法分析器的完整工程链路。

文档来龙去脉:为什么 Buck 仓库里放着一份 Python 2.7 文法

Python2Grammar.md 的第一行注明来源:它取自 CPython 2.7 源码树中的Grammar/Grammar文件,即 Python 2.7 解释器解析器所依据的正式文法。文件开头的注释还保留了 CPython 的原始告诫:

Changing the grammar specified in this file will most likely require corresponding changes in the parser module (../Modules/parsermodule.c) … You should also follow all the steps listed in PEP 306, "How to Change Python's Grammar"

也就是说,这份文档不是孤立的笔记,而是一份完整的语言规范级产物——它由 C 解析器直接驱动,任何产生式的改动都必须同步修改解析模块。

它被收录在 Buck 的tools/psi-grammar模块下,是因为该模块正是 ideabuck(Buck 的 IntelliJ 插件)的词法/语法分析器所在。同目录下的 StarlarkLanguageOverview.md 与 StarlarkLanguageSpecification.md 分别摘录了 Starlark(Bazel 构建语言,Buck 的 BUCK/DEPS 文件语言与其同源)的概述与形式规范,而 Python2Grammar.md 则是这套文档体系中的基准参照系:Buck 文件本质上是 Starlark,Starlark 在语法上又是 Python 2/3 的子集,所以 Python 2.7 的完整文法就是裁剪出 Starlark 文法的"全量母版"。

最能体现这一关系的,是 NextGenIdeabuckGrammar.bnf 文件头的说明(L17-L32):

// This file is not in use by Buck, and it is not the actual Buck grammar. // Instead, it is a prototype for a Buck grammar that more closely matches // the Python2 grammar, with specific exceptions for the Starlark language … // For reference, the full grammar of … appears below in comments, with // Buck's grammar interspersed throughout.

即:该文件把 Python2Grammar.md 的每一条产生式原文都以// [Python2]:注释形式逐行引在正文中,再在其间穿插插值(interspersed)Buck 自己的对应产生式,并在两者不一致处给出理由。下文第 2~8 节按原文档结构完整解读这份 Python 文法,第 9 节再做逐条对照。

文法记法约定:如何阅读这份 BNF

在逐条讲解之前,先明确这份文法的记法(BNF 的变体,也是 Grammar-Kit.bnf文件的通用记法):

  • 形如nonterminal: rhs的每一行是一条产生式,左部是小写非终结符,右部是终结符与非终结符的序列;
  • 带单引号的串(如'def'、'+')是字面量终结符;不带引号的大写标识符(NAME、NUMBER、STRING、NEWLINE、INDENT、DEDENT、ENDMARKER)是词法记号(token),由词法器产生;
  • [ x ]表示 x 可选(0 或 1 次);
  • x*表示 0 次或多次;x+表示 1 次或多次;
  • ( a | b )是选择;
  • 词法层面还有一个关键约定:INDENT/DEDENT是逻辑记号,由缩进推导出来,这与 Python "以缩进定块" 的语法直接对应。

Python2Grammar.md 全文约 144 行,覆盖了 Python 2.7 的全部语法。下面按原文档的行文顺序完整给出各段产生式并逐段讲解。

起始符号:single_input、file_input 与 eval_input

文法开篇定义了三个起始符号(entry points),对应三种输入场景:

single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE file_input: (NEWLINE | stmt)* ENDMARKER eval_input: testlist NEWLINE* ENDMARKER
  • single_input:交互式单行输入(python 解释器的 REPL 模式)。它要么是空行(NEWLINE),要么是一条简单语句,要么是一条复合语句后跟换行——原文注释特意强调 "compound_stmt in single_input is followed by extra NEWLINE",因为交互式环境下复合语句需要用户敲一个空行来结束缩进块;
  • file_input:从文件读入的模块,即buck解析 BUCK 文件、Python 编译.py文件所走的路径——任意多条空行或语句,以ENDMARKER(文件结束记号)收尾;
  • eval_input:eval()/input()的输入,只允许一个测试列表(testlist),因为 eval 的对象是表达式而非语句。

值得对照的一点是:Buck 的实际文法 Buck.bnf 中,对应的顶层产生式简化为buckFile ::= statement*(L130),不再区分交互/文件两种模式——这与 BUCK 文件"永远按文件编译"的定位一致。

函数定义、装饰器与参数列表

decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE decorators: decorator+ decorated: decorators (classdef | funcdef) funcdef: 'def' NAME parameters ':' suite parameters: '(' [varargslist] ')' varargslist: ((fpdef ['=' test] ',')* ('*' NAME [',' '**' NAME] | '**' NAME) | fpdef ['=' test] (',' fpdef ['=' test])* [',']) fpdef: NAME | '(' fplist ')' fplist: fpdef (',' fpdef)* [',']

这段产生式刻画了def与装饰器的完整形态:

  • funcdef要求'def'关键字、一个NAME、参数表、冒号,以及一个语句块suite;
  • varargslist的三分支结构精确对应 Python 2.7 的参数规则:
    1. 前置若干具名参数(fpdef ['=' test] ','),每个可带任意表达式默认值;
    2. 或者'*' NAME [',' '**' NAME](*args,可再跟**kwargs);
    3. 或者**' NAME(仅关键字展开);
  • fpdef: NAME | '(' fplist ')'允许在参数表中写括号分组(如def f((a, b)): ...),这一特性在 Python 3 中已被移除——它本身就是 Python 2 的语法遗产。

在 Starlark 侧的对照中,这条产生式被大幅简化。Buck.bnf 中的对应写法是:

parameter_list ::= [parameter (',' parameter)* ','?] parameter ::= '**' identifier | '*' identifier | identifier ['=' simple_expression]

NextGenIdeabuckGrammar.bnf 对此的注释引了 Starlark 规范(StarSpec:107):"The grammar does not enforce the legal order of params and args"——Starlark 有意不强制*args/**kwargs必须排最后,于是文法上直接用更宽松的产生式。而 Python 2.7 原版的decorator/decorators/decorated三条产生式在 Buck 文法中则被整条删除,注释为 "No evidence that Starlark or Buck support decorators"(NextGenIdeabuckGrammar.bnf#L331)。

语句层级:simple_stmt 与 small_stmt

stmt: simple_stmt | compound_stmt simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE small_stmt: (expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt | import_stmt | global_stmt | exec_stmt | assert_stmt)

stmt是二选一:简单语句或复合语句。simple_stmt允许用分号把多条small_stmt串在一行,行尾还可再跟一个分号([';']),以NEWLINE结束。

small_stmt的 8 个分支覆盖了 Python 2 的全部简单语句,逐条看:

赋值、增广赋值与 print

expr_stmt: testlist (augassign (yield_expr|testlist) | ('=' (yield_expr|testlist))*) augassign: ('+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=' | '**=' | '//=') print_stmt: 'print' ( [ test (',' test)* [','] ] | '>>' test [ (',' test)+ [','] ] )
  • expr_stmt的testlist开头支持链式赋值(a = b = 1)与元组解包(x, y = y, x);目标位置可以是yield_expr|testlist,这为a = b = yield x这类写法留了语法空间(是否合法由解释器进一步约束,注释 "For normal assignments, additional restrictions enforced by the interpreter" 即指此);
  • augassign的 12 个算子与//地板除一起,是 Python 2 的完整增广赋值集合(Starlark 只保留了+= -= *= /= //= %=,见 Buck.bnf#L171 的augment_assignment);
  • print_stmt把print当语句而非函数,第二个分支'>>' test支持向文件对象重定向(print >> f, x)——这是 Python 2 与 3 之间最著名的差异之一。

del、pass 与流程控制

del_stmt: 'del' exprlist pass_stmt: 'pass' flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt break_stmt: 'break' continue_stmt: 'continue' return_stmt: 'return' [testlist] yield_stmt: yield_expr raise_stmt: 'raise' [test [',' test [',' test]]]

注意raise_stmt的三元形式'raise' test ',' test ',' test:这是 Python 2 特有的raise Exc, value, tb三参异常抛出(Python 3 改为raise exc from cause)。yield_stmt独立成句,配合文末的yield_expr: 'yield' [testlist]。

import、global 与 exec

import_stmt: import_name | import_from import_name: 'import' dotted_as_names import_from: ('from' ('.'* dotted_name | '.'+) 'import' ('*' | '(' import_as_names ')' | import_as_names)) import_as_name: NAME ['as' NAME] dotted_as_name: dotted_name ['as' NAME] import_as_names: import_as_name (',' import_as_name)* [','] dotted_as_names: dotted_as_name (',' dotted_as_name)* dotted_name: NAME ('.' NAME)* global_stmt: 'global' NAME (',' NAME)* exec_stmt: 'exec' expr ['in' test [',' test]] assert_stmt: 'assert' test [',' test]
  • import_from的第一个选择项'.'* dotted_name | '.'+精确刻画了相对导入:from .mod import x(点号串后跟模块名)与from . import x(纯点号串)两种形态;'import' '*'即from x import *;
  • exec_stmt是 Python 2 的exec语句(Python 3 降格为函数),['in' test [',' test]]对应exec code in globals, locals的执行环境参数;
  • assert_stmt的可选第二参数是断言失败信息:assert cond, "msg"。

Starlark 对照:上述分支在 Buck.bnf 中被收敛为 8 个 simple 分支之一的新增条目load_call,同时删除了print、del、import、global、exec、raise、yield。其中load是 Starlark 独有的模块加载语句,其产生式(Buck.bnf#L157-L167):

load_call ::= 'load' '(' load_target_argument (',' load_argument) (',' load_argument)* [','] ')' load_target_argument ::= string load_argument ::= [identifier '='] string

比import_from严格得多:第一个参数必须是字符串字面量(目标标签),其余参数只能是"sym" = "orig_name"形式的字符串映射,不支持*、不支持*args/**kwargs。NextGenIdeabuckGrammar.bnf#L362-L367 的注释也明确:"the syntax of a load statement is much more restrictive than a normal argument_list"。

复合语句与 suite:缩进如何进入文法

compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite] while_stmt: 'while' test ':' suite ['else' ':' suite] for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite] try_stmt: ('try' ':' suite ((except_clause ':' suite)+ ['else' ':' suite] ['finally' ':' suite] | 'finally' ':' suite)) with_stmt: 'with' with_item (',' with_item)* ':' suite with_item: test ['as' expr] # NB compile.c makes sure that the default except clause is last except_clause: 'except' [test [('as' | ',') test]] suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT

这段是 Python "块语法"的核心,逐点说明:

  • if_stmt的('elif' test ':' suite)*把任意多 elif 表达为零次或多次重复;
  • for/while均支持可选的else块(循环未被break打断时执行);
  • try_stmt是一个精巧的选择结构:要么是"至少一个except_clause,可再跟else、finally",要么是纯'finally' ':' suite。原文注释 "NB compile.c makes sure that the default except clause is last" 点出了一个重要事实:裸except:(不带异常类型的默认子句)必须放在最后,这一约束文法本身不表达,由编译模块 compile.c 负责检查——这是"文法 + 语义检查"分工的典型例子;
  • with_item: test ['as' expr]让with open(f) as fh:中的绑定目标是一个通用表达式,支持with a, b as x, c as y:的元组绑定;
  • suite是全文法中INDENT/DEDENT出现的地方:要么同一行的单条简单语句,要么是NEWLINE INDENT stmt+ DEDENT的缩进块。词法器把物理缩进翻译成这对逻辑记号,文法层面就只剩普通的记号匹配了。

Starlark 对照:Buck.bnf#L183-L189 中compound_statement ::= if_statement | for_statement | function_definition——while、try、with、classdef、decorated全部缺席;for_statement也去掉了['else' ':' suite]。NextGenIdeabuckGrammar.bnf#L434-L450 的注释逐条引用 Starlark 规范给出原因:Starlark "not supported: while, yield / try, raise, except, finally(用fail代替致命错误)",for循环不支持 else 子句,且"for 语句不允许出现在顶层,请折进函数"。

表达式层级:十二级优先级与 test 的两种形态

Python2Grammar.md 中最长的部分是表达式文法。它采用逐级升格的经典写法:每引入一个低优先级算子就新增一层非终结符,从条件表达式一路下沉到原子。全文如下:

test: or_test ['if' or_test 'else' test] | lambdef or_test: and_test ('or' and_test)* and_test: not_test ('and' not_test)* not_test: 'not' not_test | comparison comparison: expr (comp_op expr)* comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not' expr: xor_expr ('|' xor_expr)* xor_expr: and_expr ('^' and_expr)* and_expr: shift_expr ('&' shift_expr)* shift_expr: arith_expr (('<<'|'>>') arith_expr)* arith_expr: term (('+'|'-') term)* term: factor (('*'|'/'|'%'|'//') factor)* factor: ('+'|'-'|'~') factor | power power: atom trailer* ['**' factor]

由此得到自低到高的完整优先级序(同级左结合,**右结合):

层级非终结符算子 / 形态结合性
1lambdeflambda [varargslist] ':' test最低
2test条件式X if C else Y右递归
3or_testor左结合
4and_testand左结合
5not_testnot右递归(一元)
6comparison< > == >= <= <> != in not in is is not,可链式左结合
7expr\|(位或)左结合
8xor_expr^(位异或)左结合
9and_expr&(位与)左结合
10shift_expr<<>>左结合
11arith_expr+-左结合
12term*/%//左结合
13factor一元+-~右递归
14power**右结合
15atom/trailer字面量、括号、调用、下标、属性—

几个值得停下来细读的点:

  1. comparison: expr (comp_op expr)*表达链式比较:1 < x < 5是合法的(语义由求值器处理为1 < x and x < 5)。这正是 Starlark 明确移除的特性——NextGenIdeabuckGrammar.bnf#L479-L485 将对照注释 "Starlark does not support chained comparisons",并把产生式改写成单次比较comparison_expression ::= simple_expression [comparison_op simple_expression],同时删除'is'算子(Starlark 要求用==)。
  2. test的双重身份:test既可是一个条件三元表达式,也可是一个lambdef。而条件式的 else 分支递归回test(而非or_test),这使a if c else b if d else e这类嵌套条件式在语法上成立且按右结合解析。
  3. power: atom trailer* ['**' factor]的右结合:2 ** 3 ** 2解析为2 ** (3 ** 2) = 512;同时因为**直接挂在power层而一元算子在factor层,-x ** 2解析为-(x ** 2)而非(-x) ** 2。这是该文法里"层级顺序即语义"的最典型例证。

test以下的支撑产生式如下:

atom: ('(' [yield_expr|testlist_comp] ')' | '[' [listmaker] ']' | '{' [dictorsetmaker] '}' | '`' testlist1 '`' | NAME | NUMBER | STRING+) trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME subscriptlist: subscript (',' subscript)* [','] subscript: '.' '.' '.' | test | [test] ':' [test] [sliceop] sliceop: ':' [test] exprlist: expr (',' expr)* [','] testlist: test (',' test)* [','] lambdef: 'lambda' [varargslist] ':' test
  • atom的五个分支即五类原子:括号表达式(含生成器与推导式)、列表、字典/集合、反引号(Python 2 独有的`x`强制 repr 语法,Python 3 已移除)、标识符/数字/字符串;
  • STRING+(注意是加号)意味着相邻字符串字面量隐式拼接("a" "b"即"ab")。Starlark 禁止这种隐式拼接,要求显式用+(见 NextGenIdeabuckGrammar.bnf#L68-L81 摘录的 Starlark 差异清单),因此 Buck.bnf#L278 的atomic_expression中字符串只出现一次,且 NextGenIdeabuckGrammar.bnf#L612-L619 为此单列了 8 个字符串记号(单/双引号 × 普通/原始 × 单行/三引号)的统一string产生式;
  • subscript的三个分支覆盖...(Ellipsis)、普通索引与切片,[test] ':' [test] [sliceop]合起来支持a[:]、a[::2]、a[1:2:3]等全部切片形态;
  • power的trailer*允许obj().method[0]这类调用、下标、属性访问的任意交错序列。

推导式、生成器与 testlist_safe 的历史细节

listmaker: test ( list_for | (',' test)* [','] ) testlist_comp: test ( comp_for | (',' test)* [','] ) dictorsetmaker: ( (test ':' test (comp_for | (',' test ':' test)* [','])) | (test (comp_for | (',' test)* [','])) ) arglist: (argument ',')* (argument [','] |'*' test (',' argument)* [',' '**' test] |'**' test) # The reason that keywords are test nodes instead of NAME is that using NAME # results in an ambiguity. ast.c makes sure it's a NAME. argument: test [comp_for] | test '=' test list_iter: list_for | list_if list_for: 'for' exprlist 'in' testlist_safe [list_iter] list_if: 'if' old_test [list_iter] comp_iter: comp_for | comp_if comp_for: 'for' exprlist 'in' or_test [comp_iter] comp_if: 'if' old_test [comp_iter] testlist1: test (',' test)* # not used in grammar, but may appear in "node" passed from Parser to Compiler encoding_decl: NAME yield_expr: 'yield' [testlist]
  • listmaker与testlist_comp是同构的"列表体":开头一个test,后面要么接推导/生成子句,要么是普通逗号列表(尾逗号可选)。区别在于testlist_comp只出现在圆括号内((...)可以是生成器表达式,而[...]内的推导式走listmaker);
  • dictorsetmaker用顶层选择区分字典推导(test ':' test comp_for)与集合推导(test comp_for),以及各自的普通字面量形态({'a': 1, 'b': 2}与{1, 2})。Starlark 没有 set 也没有 float,对应地 NextGenIdeabuckGrammar.bnf#L560-L564 将其裁剪为只保留字典形态的dictmaker;
  • arglist的三分支允许f(*args)、f(*args, **kwargs)、f(**kw)等一切调用形态,且各分支都接受尾逗号;
  • argument: test [comp_for] | test '=' test里comp_for的存在正是生成器表达式的语法入口(sum(x for x in it),圆括号可省);其上方注释解释了一个精妙的设计:关键字参数名在文法层面被写成test而非NAME,因为若用NAME会产生歧义(f(x=1)中x与f(x) = 1无法区分),真正"它必须是 NAME"的约束推迟到 ast.c 检查——又一次"文法宽松、语义收紧"的分工;
  • encoding_decl: NAME单独注明 "not used in grammar, but may appear in node passed from Parser to Compiler":它是# -*- coding: xxx -*-编码声明的载体,词法层面产出但不出现在任何产生式里。

最难懂的是testlist_safe/old_test/old_lambdef三行(紧随suite之后、test之前):

# Backward compatibility cruft to support: # [ x for x in lambda: True, lambda: False if x() ] # even while also allowing: # lambda x: 5 if x else 2 # (But not a mix of the two) testlist_safe: old_test [(',' old_test)+ [',']] old_test: or_test | old_lambdef old_lambdef: 'lambda' [varargslist] ':' old_test

原文注释自述其存在是向后兼容的遗留设计,目的是同时满足两个需求而不允许混搭:

  • [ x for x in lambda: True, lambda: False if x() ]——推导式的迭代序列里允许"旧式" lambda(其函数体是old_test,不含if-else 条件式);
  • lambda x: 5 if x else 2——独立语句位置的 lambda 函数体可以是完整test(含条件式)。

若把两个 lambda 直接复用,[x for x in lambda: True if x else False, ...]会产生"for 的迭代序列边界在哪"的歧义。于是文法克隆了一套old_前缀的降级 lambda(函数体限old_test),专门供list_for: 'for' exprlist 'in' testlist_safe [list_iter]的迭代序列使用;而comp_for迭代的是or_test,comp_if的条件是old_test——推导式条件位置也因此不能写裸 lambda。这是整份文法中最能体现"文法是为了解析器实际行为而逐条打磨"的段落,理解它基本等于读懂了 BNF 处理歧义的手段。

Starlark 侧由于不支持 lambda("not supported: lambda and nested functions"),testlist_safe/old_lambdef整套机制在 NextGenIdeabuckGrammar.bnf#L587-L602 中被合并简化:list_for/list_if与comp_for/comp_if归并为一组comprehension_iter ::= comprehension_for | comprehension_if,迭代序列直接用or_expression。

类定义与其余零散产生式

classdef: 'class' NAME ['(' [testlist] ')'] ':' suite

classdef允许任意表达式作为基类(class A(B(1+2), C): ...,基类本身可以是调用结果),基类列表可以是元组(class A(B, C))。Starlark 不支持 class("use struct function" 代替,见 NextGenIdeabuckGrammar.bnf#L566-L567),该产生式在 Buck 文法中整体删除。

至此,Python2Grammar.md 从single_input到yield_expr的全部产生式(约 60 个非终结符)即已完整覆盖:3 个起始符号、10 条函数定义相关、约 30 条语句相关、约 20 条表达式相关,外加encoding_decl与yield_expr两个"语法外"辅助记号。

从 Python 2.7 到 Starlark:Buck 如何裁剪这份文法

把上文各节中散落的对照集中起来,NextGenIdeabuckGrammar.bnf 文件头部(L51-L81)摘录 Starlark 规范后,可以整理出 Buck/Starlark 相对 Python 2.7 的完整删减清单:

Python 2.7 产生式 / 特性Starlark / Buck 的处置
decorator/decorators/decorated删除(无证据 Starlark/Buck 支持装饰器)
print_stmt删除(非关键字,用内置函数风格)
del_stmt删除(改用dict.pop()等)
import_stmt(含相对导入、*)替换为受限的load_call
global_stmt/exec_stmt删除(Starlark 不允许重赋全局变量;无 exec)
raise_stmt/yield_stmt/try_stmt/with_stmt删除(用fail表达致命错误)
while_stmt、for/while的else子句删除
classdef删除(用struct()代替类)
lambdef、yield_expr、生成器表达式(argument中的comp_for入口)删除
链式比较、is/is not删除(跨类型比较未定义,与 Python 3 一致)
反引号原子`x`、<>不等号、STRING+隐式拼接删除(<>在 Buck 文法中仍保留为comparison_op的历史遗存,见 Buck.bnf#L220)
testlist_safe/old_test/old_lambdef兼容机制随 lambda 一起消失,推导子句统一
fpdef的括号分组参数、varargslist的顺序约束简化为parameter_list,不强制*/**位置

同时保留并直接继承的部分(可逐条在 Buck.bnf 找到同形产生式):条件三元式、or/and/not三级、位运算四级、移位、四则、一元与**、trailer三分支、切片三形态、dict/列表推导、assert两参形式、缩进suite等。换言之,Python2Grammar.md 在这套文档里的角色就是"裁剪前的全量基准":读 Buck 文法时遇到任何一处删改,都能回到这份 Python 文法找到被删掉的原产生式。

文法如何变成解析器:JFlex 与 Grammar-Kit 的两遍生成

最后交代这份文法在 Buck 构建中的实际去向。tools/psi-grammar/README.md 说明该模块用 JFlex 生成词法器(输入*.flex)、用 JetBrains Grammar-Kit 生成解析器(输入*.bnf),开发时统一用 Buck 构建:

buck build //tools/psi-grammar:lexers //tools/psi-grammar:parsers

对应 tools/psi-grammar/BUCK 中的目标:lexersgenrule(L4-L17)以 Buck.flex 和 Bcfg.flex 为源;parsers目标则实现了 README 所述的两遍生成以解决"mixin 鸡生蛋"问题——Grammar-Kit 解析.bnf里的mixin = "…BuckNamedElementImpl"这类指令时需要已编译的Java 类,而编译 mixin 又依赖生成的解析器:

  1. 第一遍(BUCK#L63-L70):直接跑 Grammar-Kit 生成"不认识 mixin 的不完整解析器",打包成.src.zip;
  2. 中间步骤(BUCK#L79-L95):用这份 zip 编译 MIXIN_SRCS 中列出的 18 个手写类(如 BuckPsiImplUtil.java),并打成_custom_grammarkit可执行 JAR;
  3. 第二遍(BUCK#L98-L105):用带 mixin 的 Grammar-Kit 重新生成解析器,此时.bnf中function_definition、load_argument、identifier等产生式上声明的mixin/implements/methods(见 Buck.bnf#L132-L140)被正确解析为 PSI 方法(getName/setName等),最终由java_library目标grammar连同手写源码一起发布给//tools/ideabuck/...使用。

NextGenIdeabuckGrammar.bnf 自身不参与构建(文件头已声明 "This file is not in use by Buck"),它的用途是给开发者在 IntelliJ 的 Grammar-Kit + PsiViewer 实时预览窗里试验这套更贴近 Python 2 文法的原型,验证通过后再逐步迁移——这也解释了为什么 Python2Grammar.md 作为参考规范长期保留在docs/目录下。

小结

  • tools/psi-grammar/docs/Python2Grammar.md 是 CPython 2.7 解析器文法的完整副本:3 个起始符号、约 60 个非终结符,覆盖从single_input到yield_expr的全部 Python 2.7 语法,其中suite: NEWLINE INDENT stmt+ DEDENT与 12 级表达式分层是理解"缩进如何进入文法"与"优先级如何由非终结符层级编码"的关键;
  • 它在 Buck 仓库中的定位是基准参照:NextGenIdeabuckGrammar.bnf 逐行引用它并在间隙给出 Starlark 的删改与理由,Buck.bnf 是实际参与构建的裁剪版文法;
  • 从文法文件到可运行的 IDE 解析器,链路是 JFlex(词法)+ Grammar-Kit 两遍生成(语法,解决 mixin 编译依赖),构建入口见 tools/psi-grammar/BUCK 与 tools/psi-grammar/README.md。
  • 开发工具
  • 构建工具

【免费下载链接】buck

A fast build system that encourages the creation of small, reusable modules over a variety of platforms and languages.

项目地址:https://gitcode.com/gh_mirrors/bu/buck
点击查看免费下载

相关推荐

上一篇:如何快速上手InsightFace_Pytorch:10分钟搭建人脸识别系统
下一篇:InfiniteTalk终极指南:用AI魔法让图片和视频开口说话

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 3:26:17

水泥砂浆质保多久?从污染控制到服务部闭环的工程质量管理指南

做工程的人应该都有过这种体验&#xff1a;水泥砂浆交付之后&#xff0c;业主问的第一句话往往不是“做得怎么样”&#xff0c;而是“这个质保多久”。这问题听着简单&#xff0c;背后牵扯的却是一条完整的质量链条——材料本身有没有问题、施工有没有碰红线、交付之后谁在负责…

作者头像 李华
网站建设 2026/9/25 3:24:31

源码级拆解EastDraw:从编译到二次开发的矢量绘图实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华