GLSL Shader的格式化算法(LALR解析器)
GLSL Shader的格式化算法(LALR解析器)
在进行OpenGL程序开发时,我需要自行解析string
类型的Shader代码,抽取出里面的某些变量名和subroutine名。
由于找不到可用的GLSL Shader解析器,就照着虎书(《现代编译原理-c语言描述》)自己写了个LALR Generator,实际上包含了(词法分析器+语法分析器+格式化框架)的(LR(0)、SLR(1)、LALR(1)、LR(1))全自动生成,支持yacc的优先级指令和Shift/Reduce、Reduce/Reduce冲突解决功能。
本文的GLSL Shader的格式化工具(下载链接在这里 ),就是用我的LALR Generator,根据GLSL4.60.8的文法生成的,如下所述。
点击查看 GLSL4.60.8的文法
translation_unit :
external_declaration
| translation_unit external_declaration ;
external_declaration :
function_definition
| declaration
| ';' ;
function_definition :
function_prototype compound_statement/* compound_statement_no_new_scope */ ;
variable_identifier :
'identifier' ;
primary_expression :
variable_identifier
| 'intConstant' | 'uintConstant' | 'floatConstant' | 'boolConstant' | 'doubleConstant'
| '(' expression ')' ;
postfix_expression :
primary_expression
| postfix_expression '[' integer_expression ']'
| function_call
| postfix_expression '.' 'identifier' // FIELD_SELECTION
| postfix_expression '++'
| postfix_expression '--' ;
integer_expression :
expression ;
function_call :
function_call_or_method ;
function_call_or_method :
function_call_generic ;
function_call_generic :
function_call_header_with_parameters ')'
| function_call_header_no_parameters ')' ;
function_call_header_no_parameters :
function_call_header 'void'
| function_call_header ;
function_call_header_with_parameters :
function_call_header assignment_expression
| function_call_header_with_parameters ',' assignment_expression ;
function_call_header :
function_identifier '(' ;
function_identifier :
type_specifier
| postfix_expression ;
unary_expression :
postfix_expression
| '++' unary_expression
| '--' unary_expression
| unary_operator unary_expression ;
unary_operator :
'+' | '-' | '!' | '~' ;
multiplicative_expression :
unary_expression
| multiplicative_expression '*' unary_expression
| multiplicative_expression '/' unary_expression
| multiplicative_expression '%' unary_expression ;
additive_expression :
multiplicative_expression
| additive_expression '+' multiplicative_expression
| additive_expression '-' multiplicative_expression ;
shift_expression :
additive_expression
| shift_expression '<<' additive_expression
| shift_expression '>>' additive_expression ;
relational_expression :
shift_expression
| relational_expression '<' shift_expression
| relational_expression '>' shift_expression
| relational_expression '<=' shift_expression
| relational_expression '>=' shift_expression ;
equality_expression :
relational_expression
| equality_expression '==' relational_expression
| equality_expression '!=' relational_expression ;
and_expression :
equality_expression
| and_expression '&' equality_expression ;
exclusive_or_expression :
and_expression
| exclusive_or_expression '^' and_expression ;
inclusive_or_expression :
exclusive_or_expression
| inclusive_or_expression '|' exclusive_or_expression ;
logical_and_expression :
inclusive_or_expression
| logical_and_expression '&&' inclusive_or_expression ;
logical_xor_expression :
logical_and_expression
| logical_xor_expression '^^' logical_and_expression ;
logical_or_expression :
logical_xor_expression
| logical_or_expression '||' logical_xor_expression ;
conditional_expression :
logical_or_expression
| logical_or_expression '?' expression ':' assignment_expression ;
assignment_expression :
conditional_expression
| unary_expression assignment_operator assignment_expression ;
assignment_operator :
'='
| '*=' | '/=' | '%='
| '+=' | '-='
| '<<=' | '>>='
| '&=' | '^=' | '|=' ;
expression :
assignment_expression
| expression ',' assignment_expression ;
constant_expression :
conditional_expression ;
declaration :
function_prototype ';'
| init_declarator_list ';'
| 'precision' precision_qualifier type_specifier ';'
| type_qualifier 'identifier' '{' struct_declaration_list '}' ';'
| type_qualifier 'identifier' '{' struct_declaration_list '}' 'identifier' ';'
| type_qualifier 'identifier' '{' struct_declaration_list '}' 'identifier' array_specifier ';'
| type_qualifier ';'
| type_qualifier 'identifier' ';'
| type_qualifier 'identifier' identifier_list ';' ;
identifier_list :
',' 'identifier'
| identifier_list ',' 'identifier' ;
function_prototype :
function_declarator ')' ;
function_declarator :
function_header
| function_header_with_parameters ;
function_header_with_parameters :
function_header parameter_declaration
| function_header_with_parameters ',' parameter_declaration ;
function_header :
fully_specified_type 'identifier' '(' ;
parameter_declarator :
type_specifier 'identifier'
| type_specifier 'identifier' array_specifier ;
parameter_declaration :
type_qualifier parameter_declarator
| parameter_declarator
| type_qualifier parameter_type_specifier
| parameter_type_specifier ;
parameter_type_specifier :
type_specifier ;
init_declarator_list :
single_declaration
| init_declarator_list ',' 'identifier'
| init_declarator_list ',' 'identifier' array_specifier
| init_declarator_list ',' 'identifier' array_specifier '=' initializer
| init_declarator_list ',' 'identifier' '=' initializer ;
single_declaration :
fully_specified_type
| fully_specified_type 'identifier'
| fully_specified_type 'identifier' array_specifier
| fully_specified_type 'identifier' array_specifier '=' initializer
| fully_specified_type 'identifier' '=' initializer ;
fully_specified_type :
type_specifier
| type_qualifier type_specifier ;
invariant_qualifier :
'invariant' ;
interpolation_qualifier :
'smooth' | 'flat' | 'noperspective' ;
layout_qualifier :
'layout' '(' layout_qualifier_id_list ')' ;
layout_qualifier_id_list :
layout_qualifier_id
| layout_qualifier_id_list ',' layout_qualifier_id ;
layout_qualifier_id :
'identifier'
| 'identifier' '=' constant_expression
| 'shared' ;
precise_qualifier :
'precise' ;
type_qualifier :
single_type_qualifier
| type_qualifier single_type_qualifier ;
single_type_qualifier :
storage_qualifier
| layout_qualifier
| precision_qualifier
| interpolation_qualifier
| invariant_qualifier
| precise_qualifier ;
storage_qualifier :
'const' | 'in' | 'out' | 'inout'
| 'centroid' | 'patch' | 'sample' | 'uniform' | 'buffer'
| 'shared' | 'coherent' | 'volatile' | 'restrict'
| 'readonly' | 'writeonly'
| 'subroutine' | 'subroutine' '(' type_name_list ')' ;
type_name_list :
'type_name'
| type_name_list ',' 'type_name' ;
type_specifier :
type_specifier_nonarray
| type_specifier_nonarray array_specifier ;
array_specifier :
'[' ']'
| '[' conditional_expression ']'
| array_specifier '[' ']'
| array_specifier '[' conditional_expression ']' ;
type_specifier_nonarray :
'void' | 'float' | 'double' | 'int' | 'uint' | 'bool'
| 'vec2' | 'vec3' | 'vec4' | 'dvec2' | 'dvec3' | 'dvec4'
| 'bvec2' | 'bvec3' | 'bvec4' | 'ivec2' | 'ivec3' | 'ivec4'
| 'uvec2' | 'uvec3' | 'uvec4' | 'mat2' | 'mat3' | 'mat4'
| 'mat2x2' | 'mat2x3' | 'mat2x4' | 'mat3x2' | 'mat3x3' | 'mat3x4'
| 'mat4x2' | 'mat4x3' | 'mat4x4' | 'dmat2' | 'dmat3' | 'dmat4'
| 'dmat2x2' | 'dmat2x3' | 'dmat2x4' | 'dmat3x2' | 'dmat3x3' | 'dmat3x4'
| 'dmat4x2' | 'dmat4x3' | 'dmat4x4' | 'atomic_uint'
| 'sampler2D' | 'sampler3D' | 'samplerCube'
| 'sampler2DShadow' | 'samplerCubeShadow' | 'sampler2DArray' | 'sampler2DArrayShadow'
| 'samplerCubeArray' | 'samplerCubeArrayShadow'
| 'isampler2D' | 'isampler3D' | 'isamplerCube'
| 'isampler2DArray' | 'isamplerCubeArray'
| 'usampler2D' | 'usampler3D' | 'usamplerCube'
| 'usampler2DArray' | 'usamplerCubeArray'
| 'sampler1D' | 'sampler1DShadow' | 'sampler1DArray' | 'sampler1DArrayShadow'
| 'isampler1D' | 'isampler1DArray' | 'usampler1D' | 'usampler1DArray'
| 'sampler2DRect' | 'sampler2DRectShadow' | 'isampler2DRect' | 'usampler2DRect'
| 'samplerBuffer' | 'isamplerBuffer' | 'usamplerBuffer'
| 'sampler2DMS' | 'isampler2DMS' | 'usampler2DMS'
| 'sampler2DMSArray' | 'isampler2DMSArray' | 'usampler2DMSArray'
| 'image2D' | 'iimage2D' | 'uimage2D'
| 'image3D' | 'iimage3D' | 'uimage3D'
| 'imageCube' | 'iimageCube' | 'uimageCube'
| 'imageBuffer' | 'iimageBuffer' | 'uimageBuffer'
| 'image1D' | 'iimage1D' | 'uimage1D'
| 'image1DArray' | 'iimage1DArray' | 'uimage1DArray'
| 'image2DRect' | 'iimage2DRect' | 'uimage2DRect'
| 'image2DArray' | 'iimage2DArray' | 'uimage2DArray'
| 'imageCubeArray' | 'iimageCubeArray' | 'uimageCubeArray'
| 'image2DMS' | 'iimage2DMS' | 'uimage2DMS'
| 'image2DMSArray' | 'iimage2DMSArray' | 'uimage2DMSArray'
| struct_specifier | 'type_name' ;
precision_qualifier :
'highp' | 'mediump' | 'lowp' ;
struct_specifier :
'struct' 'type_name'/* 'identifier' */ '{' struct_declaration_list '}'
| 'struct' '{' struct_declaration_list '}' ;
struct_declaration_list :
struct_declaration
| struct_declaration_list struct_declaration ;
struct_declaration :
type_specifier struct_declarator_list ';'
| type_qualifier type_specifier struct_declarator_list ';' ;
struct_declarator_list :
struct_declarator
| struct_declarator_list ',' struct_declarator ;
struct_declarator :
'identifier'
| 'identifier' array_specifier ;
initializer :
assignment_expression
| '{' initializer_list '}'
| '{' initializer_list ',' '}' ;
initializer_list :
initializer
| initializer_list ',' initializer ;
declaration_statement :
declaration ;
statement :
compound_statement
| simple_statement ;
simple_statement :
declaration_statement
| expression_statement
| selection_statement
| switch_statement
| case_label
| iteration_statement
| jump_statement ;
compound_statement :
'{' '}'
| '{' statement_list '}' ;
/* merge into statement
statement_no_new_scope :
compound_statement_no_new_scope
| simple_statement ;
*/
/* merge into compound_statement
compound_statement_no_new_scope :
'{' '}'
| '{' statement_list '}' ;
*/
statement_list :
statement
| statement_list statement ;
expression_statement :
';'
| expression ';' ;
selection_statement :
'if' '(' expression ')' selection_rest_statement ;
selection_rest_statement :
statement 'else' statement
| statement ;
condition :
expression
| fully_specified_type 'identifier' '=' initializer ;
switch_statement :
'switch' '(' expression ')' '{' switch_statement_list '}' ;
switch_statement_list :
empty
| statement_list ;
case_label :
'case' expression ':'
| 'default' ':' ;
iteration_statement :
'while' '(' condition ')' statement/* statement_no_new_scope */
| 'do' statement 'while' '(' expression ')' ';'
| 'for' '(' for_init_statement for_rest_statement ')' statement/* statement_no_new_scope */ ;
for_init_statement :
expression_statement
| declaration_statement ;
conditionopt :
condition
| empty ;
for_rest_statement :
conditionopt ';'
| conditionopt ';' expression ;
jump_statement :
'continue' ';' | 'break' ';'
| 'return' ';'
| 'return' expression ';'
| 'discard' ';' ;
// lexical statements
// no need : 'struct' 'identifier' '{' struct_declaration_list '}'
// now I changed it into 'struct' 'type_name' '{' struct_declaration_list '}'
// only identifier next to 'struct' is a user-defined type and should be a 'type_name' token.
%%<'struct'>[a-zA-Z_][a-zA-Z0-9_]*%% 'type_name'
%%subroutine%% 'subroutine' subroutine0
<subroutine0>%%[(]%% '(' subroutine1
<subroutine1>%%[a-zA-Z_][a-zA-Z0-9_]*%% 'type_name'
<subroutine1>%%[,]%% ','
<subroutine1>%%[)]%% ')' default
%%[-+]?[0-9]+%% 'intConstant'
%%0x[0-9A-Fa-f]+%% 'intConstant'
%%[-+]?[0-9]+[uU]%% 'uintConstant'
%%0x[0-9A-Fa-f]+[uU]%% 'uintConstant'
%%[-+]?[0-9]+([.][0-9]+)?([Ee][-+]?[0-9]+)?[fF]%% 'floatConstant'
%%true/[^a-zA-Z0-9_]%% 'boolConstant'
%%false/[^a-zA-Z0-9_]%% 'boolConstant'
%%[-+]?[0-9]+([.][0-9]+)?([Ee][-+]?[0-9]+)?%% 'doubleConstant'
%%[a-zA-Z_][a-zA-Z0-9_]*%% 'identifier'
%grammarName GLSL
%blockComment on
%inlineComment on
有了解析器,就有了单词流List<Token>
和语法树Node
。面对GLSL代码的语法树,我们如何对源代码进行格式化呢?
格式化的目的是让源代码从书写上更适宜人类阅读。具体来说,格式化要做的事,只是增减某些空白符(空格、tab符、换行符)
而已,不会修改具有语法意义的内容。
在C#中,
if (x >0) {t = x;x = 0;
}
会被格式化为
if (x > 0) {
t = x; x = 0;
}
而
if (x > 0) {t = x;
x= 0;
}
会被格式化为
if (x > 0) {
t = x;
x = 0;
}
同样意义的源代码,格式化结果却不同。这有些复杂。
我们将问题拆开,一步一步来。
首先,我们忽略多行注释blockComment
、单行注释inlineComment
和源代码中原有的空白符。
此时,一个if(x>0){t=x;x=0;}
就应当被格式化为:
if (x > 0) {
t = x;
x = 0;
}
也就是说,语法树中的每一部分,都可以自己判断出应当如何格式化。用递归的方式遍历语法树即可。
例如,在处理assignment_operator : '=' | '*=' | '/=' | '%=' | '+=' | '-=' | '<<=' | '>>=' | '&=' | '^=' | '|=' ;
这个Vn结点时,其格式化地输出方法就很简单:
public void Format(TextWriter writer, FormatContext context) {
writer.Write(this.token.value);
}
对于需要处理下级结点的,例如compound_statement : '{' '}' | '{' statement_list '}' ;
这个Vn结点,其格式化地输出方法就需要按间接递归的方式依次输出各个结点:
public void Format(TextWriter writer, FormatContext context) {
writer.Write(this.tkLeftBrace.value); writer.WriteLine();
if (this.statement_list != null) {
context.IncreaseTab();
context.PrintTab(writer);
this.statement_list.Format(writer, context);
writer.WriteLine();
context.DecreaseTab();
context.PrintTab(writer);
}
writer.Write(this.tkRightBrace.value);
}
现在来考虑注释。
注释可能出现在List<Token>
(词法分析器的分析结果)里的任意位置上,属实调皮。于是就有这样一个想法,利用C#的yield return
语法糖,我们执行下述算法:
public IEnumerable<string> YieldTokens(TextWriter writer, FormatContext context) {
var enumerable = this.translation_unit.YieldTokens(writer, context);
var enumerator = enumerable.GetEnumerator();
// 遍历 List<Token>
for (int i = 0; i < context.tokens.Count; i++) {
var token = context.tokens[i];
// 如果下一个要进行格式化的Token是注释,那么就在当前位置输出它并换行。
if (token.type == CompilerGLSL.st.blockComment块) {
PrintBlockComment(token, writer, context);
}
else if (token.type == CompilerGLSL.st.inlineComment行) {
PrintInlineComment(token, writer, context);
}
else {
// 如果下一个要进行格式化的Token不是注释,
// 那么就用`yield return`的方式,
// 在语法树上格式化地输出它(输出它+某些空白符)。
enumerator.MoveNext();
// this line prints token.
var formattedString = enumerator.Current;
}
}
yield return string.Empty;
}
只要在语法树每输出一个Token
时,都进行一次yield return
(return什么无所谓),那么yield return
就能和遍历List<Token>
的过程同步完成。
例如,在处理assignment_operator : '=' | '*=' | '/=' | '%=' | '+=' | '-=' | '<<=' | '>>=' | '&=' | '^=' | '|=' ;
这个Vn结点时,其格式化地输出方法就很简单,因为它没有下级结点,从而不需要处理下级:
public IEnumerable<string> YieldTokens(TextWriter writer, FormatContext context) {
writer.Write(this.token.value);
yield return this.token.value;
}
对于需要处理下级结点的,例如compound_statement : '{' '}' | '{' statement_list '}' ;
这个Vn结点,其格式化地输出方法就需要调用每个子结点的YieldTokens(..)
,以保持yield return
与遍历List<Token>
的过程同步进行:
public override IEnumerable<string> YieldTokens(TextWriter writer, FormatContext context) {
writer.Write(this.tkLeftBrace.value); writer.WriteLine();
yield return this.tkLeftBrace.value;
if (this.statement_list != null) {
context.IncreaseTab();
context.PrintTab(writer);
// 利用yield return语法糖与遍历List<Token>的过程保持同步
foreach (var item in this.statement_list.YieldTokens(writer, context)) {
yield return item;
}
writer.WriteLine();
context.DecreaseTab();
context.PrintTab(writer);
}
writer.Write(this.tkRightBrace.value);
yield return this.tkRightBrace.value;
}
现在能够带着注释格式化了,但是无法解决这样的情况:上一行和下一行代码之间有好几个空行(即换行符),上述格式化过程会无视之。
完全的格式化算法要继承上述方法的思路,并加强如下:
- 语法树的各类语法块
Node
分别实现自己内部的格式化办法。 - 语法块
Node
要根据上一级Node
传递来的要求,在输出自己内部的第一个Token
之前,先输出多少个换行符和空格。 - 语法块
Node
要告知自己的每个下一级Node
,它希望这些子级先输出多少个换行符和空格。 - 每个
Token
根据(上级传递来的要求+它与前一个Token
之间的位置关系(间隔多少空格和换行符))输出自己前面的空白符,而后输出自己的内容。
每个语法块Node
都服从直接上级的要求,并对各个直接下级发出要求。通过这样接力的方式,就可以通过遍历一次语法树来输出格式化的代码了。
例如,在处理assignment_operator : '=' | '*=' | '/=' | '%=' | '+=' | '-=' | '<<=' | '>>=' | '&=' | '^=' | '|=' ;
这个Vn结点时,其格式化地输出方法仍旧很简单:
public void FullFormat(BlankConfig preConfig, TextWriter writer, FormatContext context) {
context.PrintBlanksAnd(this.token, preConfig, writer);
}
对于需要处理下级结点的,例如compound_statement : '{' '}' | '{' statement_list '}' ;
这个Vn结点,其格式化地输出方法就需要根据'{'
和'}'
是否在同一行来分别处理:
public override void FullFormat(BlankConfig preConfig, TextWriter writer, FormatContext context) {
{ // print {
context.PrintBlanksAnd(this.tkLeftBrace, preConfig, writer);
}
var forceNewline = this.tkRightBrace.start.line > this.tkLeftBrace.end.line;
context.IncreaseTab();
if (this.statement_list1 != null) {
var config = new BlankConfig(inlineBlank: 1, forceNewline: false);
// comment after { need not to print in next line
context.PrintCommentsBetween(this.tkLeftBrace, this.statement_list1, config, writer);
{
var config2 = config;
config2.forceNewline = forceNewline;
// statements need to print in next line
this.statement_list1.FullFormat(config2, writer, context);
}
// print comments between statement list and }
context.PrintCommentsBetween(this.statement_list1, this.tkRightBrace, config, writer);
}
else {
var config = new BlankConfig(inlineBlank: 1, forceNewline: false);
// print comments between { and }
context.PrintCommentsBetween(this.tkLeftBrace, this.tkRightBrace, config, writer);
}
context.DecreaseTab();
{ // print }
var config = new BlankConfig(inlineBlank: 1, forceNewline);
context.PrintBlanksAnd(this.tkRightBrace, config, writer);
}
}
同学们可以下载链接中的工具查看效果,里面还有一些示例shader。
End
![]() |
微信扫码,自愿捐赠。天涯同道,共谱新篇。
微信捐赠不显示捐赠者个人信息,如需要,请注明联系方式。 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了