拆解 Reggie





5.00/5 (3投票s)
深入了解实际应用程序中的一些高级源代码生成
注意:本文不包含下载内容,但会在介绍中提供相关工具/项目的链接。
引言
Reggie 是一个根据输入的正则表达式生成验证、匹配和分词代码的工具。它可以针对 C# 和 T-SQL,并支持添加更多语言。Reggie 几乎完全通过类 ASP 的模板文件驱动,这些文件驱动着代码生成过程。这些模板文件使用最新且有些实验性的 csppg 版本进行处理。CodeProject 上有关于后者的一篇文章,但它没有最新的代码。请从 Github 获取。
Csppg
我们主要在此探索 Reggie,仅在解释某个功能时才涉及到 csppg。csppg 生成 C# 代码,该代码将运行类 ASP 的模板并生成输出。模板本身以与 ASP.NET 编译 ASP.NET 页面渲染代码类似的方式进行编译。代码本身通过一个在命令行指定的类和一个也由命令行指定的 `static` 方法公开。生成的方法接受一个 `TextWriter` 用于写入,一个 `IDictionary<string, object>` 参数,以及模板文件本身中的 `<%@param ... %>` 指令所指示的任何其他参数。
状态机代码
即使您已经拥有一个可靠的正则表达式引擎,但使用它来生成高效且正确的运行状态机以查找文本模式的代码,比听起来要难,而且当您针对多个编程环境时,尤其是在让它们在任何地方都能一致运行时,会更加困难。
状态机通过表来运行——无论是编译到代码中的 `GOTO` 表,还是通过导航实际的表或数组。最终,它们都归结为相同的基本执行流程,但有两种相当不同的实现方式。
Reggie 使用我的 `FastFA` 库,该库包含在项目中。这是一个支持 Unicode 的正则表达式引擎。它可以从正则表达式生成状态机,而这正是 Reggie 使用它的目的。所有表达式在内部都存储为 UTF-32 代码点,这也是我们在运行匹配时提供给它的内容,即使输入源是其他编码。
状态机通常被当作整数数组进行操作,其格式在 Reggie 的主文章中有描述,但本质上每个状态是后面跟着转换次数的接受整数(如果没有接受则为 -1),然后是每个转换的条目。每个转换是下一个要跳转到的状态在表中目标索引,后面跟着该转换的 `N` 个范围计数,然后是 `N` 对整数,表示范围的最小和最大字符值。当生成代码时,它是基于这些整数数组的内容创建的。
生成器代码
Reggie 使用 `csppg` 作为预构建步骤来生成 *Templates* 文件夹的内容,其输出位于 *Generators* 文件夹中。目前,Reggie 中大约有 250 个生成文件,对应于大约 250 个模板。如果您一直关注,您可能已经注意到,由于我们使用了 csppg,我们实际上是在生成代码来生成代码。多么的元啊。无论如何,每个文件都是部分类 `Generator` 的一个方法。每个方法都以其对应的模板文件名(不带扩展名)命名。
由于方法是由 csppg 生成的,它们接受必需的 `TextWriter` 和 `IDictionary<string, object>` 参数,以及前面提到的在模板文件本身中指定的任何潜在参数。
优点、缺点和丑陋之处
Generator
类这个“科学怪人”组合体是有效的,而且当它工作良好时,它表现得非常好。通过它,您实际上拥有一个方法来渲染项目中的每个模板,并且这些模板可以在其他模板中进行链式调用。这一点很重要,因为链式调用在这个项目中被广泛使用。
坏消息是,为了使其正常工作,其中存在一些棘手的问题,因为所有方法都是静态的。我们将一些重要的数据隐藏在 `Arguments` `IDictionary<string, object>` 实例中以使其正常工作。事实上,`Arguments` 字典会被增强为一个通用的调度对象,我们稍后会讨论。基本上,`Arguments` 字典应该被视为您的万能状态存储。
更坏的消息是,模板必须是后期绑定的,这可能会在调试时带来一些麻烦。根据我的经验,到目前为止并没有引起太多问题,但小的烦恼,例如当您忘记将新生成的模板代码包含到项目中时,会得到运行时错误而不是编译时错误,这可能会有点令人沮丧,尽管易于纠正。
与 ASP.NET 一样,csppg 会生成行 pragma,这些 pragma 可用于定位模板文件本身中的错误代码。没有这些,代码的失败将被报告在模板生成的 C# 文件中。这使得跟踪问题更加容易。但是,由于 C# 10 之前的版本存在限制,它最多只能精确到行,所以如果您有很长的代码行,在查看对应的 C# 文件之前,您可能不知道错误的确切位置。
TemplateCore
Generator
继承自 `TemplateCore`,后者充当服务对象,为模板提供重要的功能。可能最重要的事情是它通过 `Run()` 和 `Generate()` 方法绑定和调用模板。前者基本上是后者的低级版本。它们是后期绑定的,以便模板方法可以在运行时选择。其余的方法基本上是执行各种任务的实用方法。
Arguments Expando 和模板调度
Reggie 在模板内部调用其他模板以生成内容。这通过一个特殊的调度机制来实现,该机制根据当前 `/target` 选择模板,如果找不到目标模板,则回退到通用模板。
模板调度主要通过一个“expando”对象来实现。Expando 对象动态公开成员,这些成员可以通过 C# 中的 `dynamic` 关键字在运行时进行绑定。这有效地允许您在运行时甚至在对象的生命周期内公开新的字段、属性、方法和事件。C# 编译器使用 `DynamicObject` 的派生类在每次使用 `dynamic` 关键字作为变量类型时生成用于后期绑定调用的代码。
您通常会在模板开头看到这段代码
<%dynamic a = Arguments;%>
这允许您通过调用 `a` 来访问在运行时绑定的动态成员。Arguments 是一个 expando 对象,它会自动将字典中的每个成员公开为字段,并将每个 `Generator` 方法公开为方法。当您以这种方式调用生成器方法来调用其他模板时,当前的 Response 和 Arguments 对象会自动作为前两个参数传递给模板方法,而任何剩余的参数将根据模板本身中是否存在任何 `<%@param ... %>` 指令作为参数传递。请注意,要访问 `Arguments` 的动态性,必须将其分配给一个 `dynamic` 变量,并且必须引用该变量而不是 `Arguments`。
a.Comment("Hello World!");
考虑上面的代码。它调用了一个 *.template* 文件。它首先根据目标选择文件,因此如果目标是 `CS` (C#),则调度程序将首先尝试调用 *CSComment.template*,然后回退到 *Comment.template*。如果目标是 `SQL` (T-SQL),它将尝试 *SqlComment.template*,然后回退。请注意,所有调度程序调用都是不区分大小写的。
CSComment.template 看起来像这样**
<%@param name="text" type="string"%>// <%=text%>
** Reggie 使用一个更复杂的注释模板,该模板可以自动将多行文本转换为多个单行注释,但我不想用额外的复杂性来弄乱事情。
最后有一个换行符(由于 Code Project 重新格式化了我的代码块,此处未显示),这在这里很重要,因为它确保下一行不会出现在此行的同一行上。
SqlComment.template 看起来像这样
<%@param name="text" type="string"%>-- <%=text%>
同样,它有一个此处未显示的换行符。
还有一个或多或少为空的文件,名为 *Comment.template*:
<%@param name="text" type="string"%>
这个没有换行符,因为它不生成任何内容。
在这三个文件之间,我们将注释定义为 C# 的一种方式,T-SQL 的另一种方式,如果未为目标另行定义,则默认为无。如果没有“空”的 *Comment.template*,Reggie 将在遇到 `a.Comment(...)` 调用时出错,除非您创建一个针对目标的操作。注释不会影响结果代码的行为,因此我们可以将其默认为什么都不做。
命令行解析
我在这里介绍它,因为它属于 `Arguments` 功能的一部分。在程序开头 `Start.template` 中,`Arguments` 被填充了许多具有空值的命名值。然后将此字典传递给 `CrackArguments()`,该函数接受这些命名值并使用它们来确定命令行接受的内容,包括值的类型。应注意的是,此函数会忽略任何不以字母或数字开头的参数。然后,该函数根据命令行参数填充相关的 `Arguments` 条目值。此解析函数生成相对简单的命令行参数结构,但足以满足我们的需求。
按照惯例,所有非开关参数都以前划线 `_` 为前缀,以确保它们永远不会与命令行参数混淆。
代码格式化
虽然可读性的必要性可能不重要,因为代码是生成的,但正确格式化代码以潜在地针对具有大量空白字符的语言(如 Python)或更常见的基于行的语法(如 BASIC)仍然至关重要。
缩进
由于这个要求,Reggie 使用一个小的 `IndentedTextWriter` 类,该类会根据 `IndentLevel` 属性缩进行。可以通过 `Arguments` expando 使用 `a._indent` 来设置此属性。
代码大致看起来像这样
dynamic a = Arguments;
a._indent = ((int)a._indent) + 1;
// do indented code ...
a._indent = ((int)a._indent) - 1;
我进行强制类型转换是因为理论上 expando 的所有成员都返回 `object` 类型,但我还没有研究 `dynamic` 调用站点是否允许对 `object` 类型的实例使用数字运算符。强制类型转换可能不是必需的。
换行
换行可能比缩进更重要,因为许多语言都有基于行的语法。
这不需要代码中的任何特殊内容,只需在设计模板时多加注意。尽管近 250 个模板中可能有些有些混乱,但我通常遵循一个约定,即换行符总是 *跟随* 内容而不是在其前面。我还会注意在模板末尾添加换行符,这通常是由于上述约定所必需的。
主模板
主模板用于生成检查器、匹配器和词法分析器。这些模板始终独立于语言,允许调度系统为适当的目标选择模板。保持独立于语言的主代码模板可确保代码在目标之间在结构和功能上等效。曾经,Reggie 每个目标只使用很少的模板,但它未能通过测试,因为不同目标的生成代码在功能上不相同,而且很难使其相同。这就是为什么现在有大约 250 个非常小的模板,每个模板处理每个目标的特定操作,而不是大约 6 个覆盖每个目标 3 种主要类型的模板。
我大约会把 8 个模板归类为主模板。我们将大致按执行顺序介绍它们。
Start.template
与其他模板不同,这个模板写入 *stderr*。它的工作是解析和验证参数,查找可用的目标,加载输入文件,计算任何必需的状态表,如果指示则写入 *.dot* 和 *.jpg* 文件,然后要么启动生成,要么在发生错误时显示使用说明屏幕。它基本上取代了入口点中发生的大部分逻辑。
MainFile.template
MainFile.template 仅在文件类和命名空间中放置主要的前导和尾随代码,然后根据命令行开关路由到适当的模板。
TableChecker.template and CompiledChecker.template
这些模板使用表或编译代码来渲染验证代码(IsXXXX() 方法
)。
TableMatcher.template and CompiledMatcher.template
这些模板使用表或编译代码来渲染验证代码(MatchXXXX() 方法
)。
TableLexer.template and CompiledLexer.template
这些模板使用表或编译代码来渲染分词/词法分析代码(Tokenize() 方法
)。
为了简洁起见,我避免发布上述代码,但让我们来探讨一下词法分析代码,因为它功能最全面。请注意,下面的代码是用 *输出* 而不是源代码本身进行格式化的
dynamic a = Arguments;
var symbolTable = (string[])a._symbolTable;
var symbolFlags = (int[])a._symbolFlags;
var dDfa = (int[])a._dfa;
var blockEndDfas = (int[][])a._blockEndDfas;
a.MethodPrologue("LexerTokenizeDocumentation",false,
"LexerTokenizeReturn","Tokenize","LexerTokenizeParams");
a.TableLexerTokenizeDeclarations();
a.LexerCreateResultList();
a.ReadCodepoint(false);
a.InputLoopPrologue();
a.LexerResetMatch();
a.TableStateReset();
a.LexerClearMatched();
a.TableMachineLoopPrologue();
a.TableMove(false,false,false);
a.TableMachineLoopEpilogue();
a.TableAcceptPrologue();
a.LexerYieldPendingErrorResult(false,false);
a.TableLexerGetBlockEnd();
a.TableIfBlockEndPrologue();
a.TableLexerStoreAccept();
a.TableStateReset();
a.InputLoopPrologue();
a.ClearMatched();
a.TableMachineLoopPrologue();
a.TableMove(true,false,false);
a.TableMachineLoopEpilogue();
a.TableAcceptPrologue();
a.TableLexerYieldResult(true);
a.ClearCapture();
a.BreakInputLoop();
a.TableAcceptEpilogue();
a.UpdateLineAny();
a.AppendCapture();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.TableStateReset();
a.InputLoopEpilogue();
a.TableIfNotMatchedBlockEndPrologue();
a.LexerYieldPendingErrorResult(false,true);
a.ClearCapture();
a.TableIfNotMatchedBlockEndEpilogue();
a.ContinueInputLoop();
a.TableIfBlockEndEpilogue();
a.TableIfNotBlockEndPrologue();
a.LexerYieldPendingErrorResult(false,false);
a.TableLexerYieldNonEmptyResult(false);
a.ClearCapture();
a.TableIfNotBlockEndEpilogue();
a.TableAcceptEpilogue();
a.TableRejectPrologue();
a.LexerIfNotMatchedWithErrorPrologue();
a.UpdateLineAny();
a.AppendCapture();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.LexerIfNotMatchedWithErrorEpilogue();
a.LexerHandleError();
a.TableRejectEpilogue();
a.InputLoopEpilogue();
a.LexerYieldPendingErrorResult(true,false);
a.LexerReturnResultList();
a.MethodEpilogue();
首先,我们从 `a` 中声明一些变量以方便以后访问。我实际上并不在此模板中使用它们,但我将其保留在此处,因为这是一种我对此类模板的标准——而且它们在编译版本中使用。
之后,我们调用了无数个模板。您会注意到前导/尾随模板。它们通常在目标语言中打开一个 `if` 或 `while` 语句块,尽管它们不限于此。特别是在针对 SQL 时,某些模板会以其他语言不具备的方式进行增强,因此您可能会在导言或尾声中找到额外的代码。只要代码的实际结果与目标语言对其他目标语言的结果相同,就无关紧要。
模板名称遵循一个模式,即它们以它们适用的代码生成部分为前缀。`TableLexerXXXX()` 表示一个特定于正在生成表词法分析器的模板,而 `LexerXXXX()` 表示它适用于任何词法分析器代码(编译或表驱动),像 `ReadCodepoint()` 这样的模板则适用于所有内容。
这是编译的词法分析器模板。它要复杂得多,因为对于表驱动代码,所有复杂性都在构建表中,而这甚至不在 *TableLexer.template* 中!由于编译代码的性质,这种复杂性必须存在于 *CompiledLexer.template* 中。
dynamic a = Arguments;
var symbolTable = (string[])a._symbolTable;
var symbolFlags = (int[])a._symbolFlags;
var dfa = (int[])a._dfa;
int sid, si;
int[] map;
var blockEndDfas = (int[][])a._blockEndDfas;
for(var symId = 0;symId<blockEndDfas.Length;++symId) {
var bedfa = blockEndDfas[symId];
if(bedfa!=null) {
a.Comment("Tokenizes the block end for "+ symbolTable[symId]);
a.MethodPrologue("None",true,"CompiledLexerTokenizeBlockEndReturn",
"Tokenize"+symbolTable[symId]+"BlockEnd","CompiledLexerTokenizeBlockEndParams");
a.CompiledLexerTokenizeBlockEndDeclarations();
a.InputLoopPrologue();
a.ClearMatched();
si = 0;
sid = 0;
map = GetDfaStateTransitionMap(bedfa);
while(si < bedfa.Length) {
if(sid != 0 || IsQ0Reffed(bedfa)) {
a.Label("q"+sid.ToString());
} else {
a._indent = (int)a._indent - 1;
a.Comment("q"+sid.ToString());
a._indent = (int)a._indent + 1;
}
var acc = bedfa[si++];
var tlen = bedfa[si++];
for(var i = 0; i < tlen; ++i) {
var tto = map[bedfa[si++]];
var prlenIndex = si;
var prlen = bedfa[si++];
var rstart = prlenIndex;
var ranges = bedfa;
if((bool)a.lines) {
var lclist = new List<int>(10);
if (DfaRangesContains('\n',ranges,rstart)) {
lclist.Add('\n');
ranges = DfaExcludeFromRanges('\n',ranges,rstart);
rstart = 0;
}
if (DfaRangesContains('\r',ranges, rstart)) {
lclist.Add('\r');
ranges = DfaExcludeFromRanges('\r',ranges,rstart);
rstart = 0;
}
if (DfaRangesContains('\t',ranges, rstart)) {
lclist.Add('\t');
ranges = DfaExcludeFromRanges('\t',ranges,rstart);
rstart = 0;
}
if(lclist.Contains('\t')) {
var temprange = new int[] {1,'\t','\t'};
a.CompiledRangeMatchTestPrologue(temprange,0);
a.CompiledAppendCapture(true);
a.UpdateTab();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.SetMatched();
a.CompiledGotoState(tto);
a.CompiledRangeMatchTestEpilogue();
}
if(lclist.Contains('\n')) {
var temprange = new int[] {1,'\n','\n'};
a.CompiledRangeMatchTestPrologue(temprange,0);
a.CompiledAppendCapture(true);
a.UpdateLineFeed();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.SetMatched();
a.CompiledGotoState(tto);
a.CompiledRangeMatchTestEpilogue();
}
if(lclist.Contains('\r')) {
var temprange = new int[] {1,'\r','\r'};
a.CompiledRangeMatchTestPrologue(temprange,0);
a.CompiledAppendCapture(true);
a.UpdateCarriageReturn();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.SetMatched();
a.CompiledGotoState(tto);
a.CompiledRangeMatchTestEpilogue();
}
} // if(lines) ...
var exts = GetTransitionExtents(ranges,rstart);
a.CompiledRangeMatchTestPrologue(ranges,rstart);
a.CompiledAppendCapture(exts.Value<128);
if(exts.Value>31) {
a.UpdateNonControl(exts.Key<32);
}
a.ReadCodepoint(false);
a.AdvanceCursor();
a.SetMatched();
a.CompiledGotoState(tto);
a.CompiledRangeMatchTestEpilogue();
si+=prlen*2;
} // for(i..tlen) ...
if(acc!=-1) {
a.CompiledLexerTokenizeBlockEndAccept();
} else {
a.CompiledGotoNext();
}
++sid;
}
a.Label("next");
a.IfNotMatchedPrologue();
a.UpdateLineAny();
a.AppendCapture();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.IfNotMatchedEpilogue();
a.InputLoopEpilogue();
a.CompiledLexerTokenizeBlockEndReject();
a.MethodEpilogue();
}
}
a.MethodPrologue("LexerTokenizeDocumentation",false,"LexerTokenizeReturn",
"Tokenize","LexerTokenizeParams");
a.CompiledLexerTokenizeDeclarations();
a.LexerCreateResultList();
a.ReadCodepoint(false);
a.InputLoopPrologue();
a.LexerResetMatch();
a.LexerClearMatched();
si = 0;
sid = 0;
map = GetDfaStateTransitionMap(dfa);
while(si < dfa.Length) {
if(sid != 0 || IsQ0Reffed(dfa)) {
a.Label("q"+sid.ToString());
} else {
a._indent = (int)a._indent - 1;
a.Comment("q"+sid.ToString());
a._indent = (int)a._indent + 1;
}
var acc = dfa[si++];
var tlen = dfa[si++];
for(var i = 0; i < tlen; ++i) {
var tto = map[dfa[si++]];
var prlenIndex = si;
var prlen = dfa[si++];
var rstart = prlenIndex;
var ranges = dfa;
if((bool)a.lines) {
var lclist = new List<int>(10);
if (DfaRangesContains('\n',ranges,rstart)) {
lclist.Add('\n');
ranges = DfaExcludeFromRanges('\n',ranges,rstart);
rstart = 0;
}
if (DfaRangesContains('\r',ranges, rstart)) {
lclist.Add('\r');
ranges = DfaExcludeFromRanges('\r',ranges,rstart);
rstart = 0;
}
if (DfaRangesContains('\t',ranges, rstart)) {
lclist.Add('\t');
ranges = DfaExcludeFromRanges('\t',ranges,rstart);
rstart = 0;
}
if(lclist.Contains('\t')) {
var temprange = new int[] {1,'\t','\t'};
a.CompiledRangeMatchTestPrologue(temprange,0);
a.CompiledAppendCapture(true);
a.UpdateTab();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.LexerSetMatched();
a.CompiledGotoState(tto);
a.CompiledRangeMatchTestEpilogue();
}
if(lclist.Contains('\n')) {
var temprange = new int[] {1,'\n','\n'};
a.CompiledRangeMatchTestPrologue(temprange,0);
a.CompiledAppendCapture(true);
a.UpdateLineFeed();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.LexerSetMatched();
a.CompiledGotoState(tto);
a.CompiledRangeMatchTestEpilogue();
}
if(lclist.Contains('\r')) {
var temprange = new int[] {1,'\r','\r'};
a.CompiledRangeMatchTestPrologue(temprange,0);
a.CompiledAppendCapture(true);
a.UpdateCarriageReturn();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.LexerSetMatched();
a.CompiledGotoState(tto);
a.CompiledRangeMatchTestEpilogue();
}
} // if(lines) ...
var exts = GetTransitionExtents(ranges,rstart);
a.CompiledRangeMatchTestPrologue(ranges,rstart);
a.CompiledAppendCapture(exts.Value<128);
if(exts.Value>31) {
a.UpdateNonControl(exts.Key<32);
}
a.ReadCodepoint(false);
a.AdvanceCursor();
a.LexerSetMatched();
a.CompiledGotoState(tto);
a.CompiledRangeMatchTestEpilogue();
si+=prlen*2;
} // for(i..tlen) ...
if(acc!=-1) { // accepting
a.LexerYieldPendingErrorResult(false,false);
var bedfa = blockEndDfas[acc];
if(bedfa==null) {
if(0==(symbolFlags[acc] & 1)) {
a.CompiledLexerYieldNonEmptyResult(symbolTable[acc], acc);
}
a.ClearCapture();
a.ContinueInputLoop();
} else {
a.CompiledLexerDoBlockEndPrologue(symbolTable[acc]);
a.CompiledLexerYieldResult(symbolTable[acc], acc);
a.ClearCapture();
a.ContinueInputLoop();
a.CompiledLexerDoBlockEndEpilogue();
a.LexerYieldPendingErrorResult(false,true);
a.ClearCapture();
a.ContinueInputLoop();
}
} else { // not accepting
a.CompiledGotoError();
}
++sid;
} // while(si < dfa.Length) ...
a.Label("error");
a.LexerIfNotMatchedWithErrorPrologue();
a.UpdateLineAny();
a.AppendCapture();
a.ReadCodepoint(false);
a.AdvanceCursor();
a.LexerIfNotMatchedWithErrorEpilogue();
a.LexerHandleError();
a.InputLoopEpilogue();
a.LexerYieldPendingErrorResult(true,false);
a.LexerReturnResultList();
a.MethodEpilogue();
这部分代码很多,但它实际生成的代码会相当简单。除了遍历状态机生成范围匹配测试(如 `if(ch >= 'a' && ch <= 'z')`)和 `goto` 之外,当指定 `/lines` 时,它还会特别处理某些空白字符,以提高效率。为了避免重复检查字符是否存在制表符、换行符和回车符,它会将相关的空白字符与其包含的范围分开,并将其显示为单独的 if 语句。在该 `if` 语句内,转换的處理方式与其他转换类似,*除了* 它还处理由所涉及字符指示的相关空白字符操作。这意味着 `[\t\r\n ]+` 在这种情况下将被分解为四个不同的 `if` 语句——每个字符一个,因为其中三个是特殊控制字符,处理方式不同。这主要通过 `DfaExcludeFromRanges()` 实现,该函数接受特定目标状态的转换表开头处的有限自动机,并创建一个新的数组,该数组是该转换表,其中移除了指示的字符。您会注意到,当 `rstart` 最初设置为指向目标状态的转换表的 dfa 数组中的索引时,在调用 `DfaExcludeFromRanges()` 后,它会被设置为零。这是因为该函数返回一个新数组,所以起始索引为零。最终结果是,如果调用了该函数,rstart 将被清零,因此我们始终处理正确数组中的正确索引。
您可能会注意到,我们小心地处理 `q0:` 标签的边界情况,*仅当* 代码中存在 `goto g0;` 时才这样做,这取决于表达式。因为否则编译器会警告未引用的标签。取而代之的是,它被替换为一个与 `goto` 相同缩进级别的注释,以在视觉上模拟它。
我之前提到了空白字符,但当我们知道范围落在 7 位 ASCII 内时,我们还有另一个优化。我们可以跳过 `char.ConvertFromUtf32(ch)` 调用,直接将 `int` 转换为 `char`,而无需检查溢出。这是内部循环中的操作,因此减少开销很重要,虽然可以进行更进一步、更花哨的优化,但我将大部分推迟到以后。
支持更多语言
虽然并非易事,但您可以扩展 Reggie 以支持其他语言。如果您添加了适当的模板,Reggie 将自动在用法屏幕和命令行中使用反射发布新目标。
基本上,您需要先复制 *CSTargetGenerator.template* 中的代码行到 *XXXXTargetGenerator.template*,其中 XXXX 是您目标语言的名称,并且最好以生成文件的文件扩展名命名。Python 生成器可能应该称为 *PyTargetGenerator.template*。如果您对同一文件扩展名有多个生成器(例如,不同 RDBMS 供应商的不同 SQL 代码),您可能希望使用 `/flavor` 开关之类的东西,然后使所有模板基于该开关有条件地生成代码。
至于其他方面,我能告诉您最好的建议是尽力复制和移植 C# 模板。对于 SQL,我直接将整个 *Templates\CS* 文件夹复制到 *Templates\SQL*,然后逐个编辑和重命名每个文件。完成这些并使所有内容都能构建后,我使用了 Tests 项目来查找错误,直到它变得一致。对于您的目标语言来说,一个可能显著不同的是缺乏协程。这意味着您可能没有 `yield` 关键字的类似物。您可以创建非空的 *XXXXCreateResultList.template* 和 *XXXXReturnResultList.template* 文件来创建一个列表来保存结果,然后分别返回结果列表。您的 *XXXXYieldXXXX.template* 函数将简单地将项添加到您创建的结果列表中。这样做会丢失流式处理能力,但根据我的经验,在大多数实际场景中,流式处理的好处并不大。
接下来是什么?
现在您可以尝试通过用您想要的语言重新实现所有 CS 模板来编写新目标,或者直接复制代码,复制 Reggie 的骨架并复制预构建步骤,以创建您自己的基于模板的多语言代码生成器。
或者,您也可以直接使用 Reggie 生成代码。
无论您如何处理它,我都希望您喜欢它。
历史
- 2021 年 11 月 8 日 - 首次提交