LexContext:文本输入的流式光标






4.20/5 (3投票s)
使用这个简单的类来管理位置跟踪、生命周期、错误报告以及基本的解析/词法分析
引言
这是一个我之前讨论过的主题,但是代码变得有些陈旧且臃肿,所以我决定进行简化、重写并写一篇配套的文章。
我编写大量的解析器和词法分析器以及它们的生成器,在此过程中,我开发了一些简单的工具来处理常见的陷阱。这里介绍的类是我许多代码的基础。我在许多项目中都使用它或它的一个变体,包括Parsley。它提供了一个统一的接口,可以处理各种不同的输入源,管理生命周期、位置信息(如行号和列号)、捕获输入、错误报告,以及一个比TextReader
或IEnumerator<char>
更好的光标,所有这些都包含在一个轻量级的类中。
我将重新介绍我在导言中提到的上一篇文章中已经涵盖的内容。如果您一直跟随上一篇文章的内容,我所做的主要更改是移除了前瞻以从类中榨取一点性能,并将一些辅助方法分离到单独的文件中,使得核心API极其精简,同时将扩展功能保留在一个单独的包含文件中。
概念化这个混乱的局面
尽管API很小,但这个类涵盖了词法分析和解析的几个陷阱,所以我们将分节讨论它们。
统一输入和生命周期管理
LexContext
提供了Create()
和CreateFrom()
方法,它们分别接受IEnumerator<char>
或TextReader
。还有一个CreateFrom()
重载,接受一个文件名。此外,还有一个CreateFromUrl()
方法,允许您创建一个LexContext
来处理由URL指定的远程或本地源。
此外,LexContext
提供了Close()
方法,并且是可处置的,以便您可以关闭底层的文件或网络流(如果有的话)。
无论实例是如何创建的,访问它的接口都是相同的。
游标管理
在编写词法分析器或无词法分析器的解析器时,您通常需要保留光标下的当前字符,并将其传递给您创建的各种解析或词法分析函数。稍后我将演示,但基本上,如果您不这样做,您会发现自己在使用TextReader
的Peek()
方法,而这是很糟糕的(TM),因为它阻止您的代码在网络流或其他不可搜索的源(如控制台输入)上工作。
IEnumerator<char>
在这方面更好,因为它提供了Current
属性,该属性提供对我们所需的光标下字符的访问。然而,与TextReader
不同的是,如果光标超出了流的末尾,IEnumerator<char>
会抛出异常,并且除了在您调用MoveNext()
时,不会给出任何指示。这是非常不利的,因为它意味着您必须自己跟踪结束条件,如果必须将这种簿记纳入您的解析例程,可能会使解析代码变得相当复杂。
LexContext
,就像TextReader
一样,即使光标不在字符上也会报告当前光标值。与IEnumerator<char>
一样,它通过Current
属性报告当前字符,并且这样做时无需查找。当光标处于初始位置时,它将用-1/EndOfInput
报告输入结束,或者在开始之前用-2/BeforeInput
报告。如果LexContext
已被关闭/处置,它还将报告-3/Disposed
。
Advance()
的行为类似于TextReader.Read()
,它会移动光标并返回读取的字符。在调用Advance()
之后,可以通过Current
属性再次访问该字符。您可以根据需要使用最适合您代码的机制来检索当前字符。
使用这些可以消除上述陷阱,从而使您的词法/解析代码执行几乎变得微不足道,同时保留检查不可搜索流的能力。
位置信息
LexContext
跟踪位置、行号和列号,以及当前文档的当前文件或URL(如果有)。这通常用于错误报告,但也可以用于任何需要的目的。制表符宽度为4,除非使用TabWidth
属性另行指定。将其设置为输入设备的制表符宽度可以准确地跟踪列信息。对于控制台以及大多数底层输入源,默认值应该是合适的。Line
和Column
属性是以1为基数的,而Position
属性是以0为基数的。FileOrUrl
包含当前文档的源位置。
错误处理
ExpectingException
是在词法分析器/解析器遇到错误时抛出的异常。它报告错误位置信息、消息以及可选的预期字符或符号集合。您可以手动抛出它,但通常它是在调用Expecting()
的结果中引发的。
在解析过程中,您调用Expecting()
来指示您期望Current
是您期望的字符系列中的一个。如果Current
不是其中一个字符,则会自动填充包含当前上下文信息的ExpectingException
并将其抛出。
捕获输入
在解析或词法分析时,您通常需要捕获您正在检查的输入,以便在解析/词法分析期间稍后检索,作为子解析的一部分,或者仅仅是为了从您的词法分析或解析函数中报告。LexContext
提供了一个CaptureBuffer
来帮助完成这项工作。
CaptureBuffer
本身就是一个StringBuilder
,而LexContext
提供了Capture()
来捕获光标下的当前字符(如果存在),GetCapture()
来检索全部或部分捕获的缓冲区,以及ClearCapture()
来清除捕获缓冲区。
如果您需要执行子解析,只需将您需要子解析的内容捕获到捕获缓冲区中,然后为捕获的字符串创建一个新的LexContext
!然后,您可以将此LexContext
馈送给您的子解析函数。
使用这个烂摊子
我提供了一个简单的JSON最小化器作为示例。它大约是JSON解析器的一半,因为它将解析后的文本加载到字典和列表中,具体取决于它们是JSON对象还是JSON数组。它没有做的是规范化标量值(如字符串)。字符串将返回带有引号和嵌入转义字符,数字、布尔值和null将作为字符串返回。
static void Main(string[] args)
{
// minifies JSON. Does so by parsing into an intermediary graph
// this step wasn't required, but makes it easier to adapt
// the code to a real world JSON parser
// holds our json data
IDictionary<string, object> json = null;
// parse our file
using (var pc = LexContext.CreateFrom(@"..\..\Burn Notice.2919.tv.json"))
json = _ParseJsonObject(pc);
// write our json data out
_WriteJsonTo(json, Console.Out);
}
static object _ParseJson(LexContext pc)
{
// parses a JSON object, array, or value
pc.TrySkipWhiteSpace();
switch (pc.Current)
{
case '{':
return _ParseJsonObject(pc);
case '[':
return _ParseJsonArray(pc);
default:
return _ParseJsonValue(pc);
}
}
static IDictionary<string, object> _ParseJsonObject(LexContext pc)
{
// a JSON {} object - our objects are dictionaries
var result = new Dictionary<string, object>();
pc.TrySkipWhiteSpace();
pc.Expecting('{');
pc.Advance();
pc.Expecting(); // expecting anything other than end of input
while ('}' != pc.Current && -1 != pc.Current) // loop until } or end
{
pc.TrySkipWhiteSpace();
// _ParseJsonValue parses any scalar value, but we only want
// a string so we check here that there's a quote mark to
// ensure the field will be a string.
pc.Expecting('"');
var fn = _ParseJsonValue(pc);
pc.TrySkipWhiteSpace();
pc.Expecting(':');
pc.Advance();
// add the next value to the dictionary
result.Add(fn, _ParseJson(pc));
pc.TrySkipWhiteSpace();
pc.Expecting('}', ',');
// skip commas
if (',' == pc.Current) pc.Advance();
}
// make sure we're positioned on the end
pc.Expecting('}');
// ... and read past it
pc.Advance();
return result;
}
static IList<object> _ParseJsonArray(LexContext pc)
{
// a JSON [] array - our arrays are lists
var result = new List<object>();
pc.TrySkipWhiteSpace();
pc.Expecting('[');
pc.Advance();
pc.Expecting(); // expect anything but end of input
// loop until end of array or input
while (-1 != pc.Current && ']' != pc.Current)
{
pc.TrySkipWhiteSpace();
// add the next item
result.Add(_ParseJson(pc));
pc.TrySkipWhiteSpace();
pc.Expecting(']', ',');
// skip the comma
if (',' == pc.Current) pc.Advance();
}
// ensure we're on the final position
pc.Expecting(']');
// .. and read past it
pc.Advance();
return result;
}
static string _ParseJsonValue(LexContext pc)
{
// parses a scalar JSON value, represented as a string
// strings are returned quotes and all, with escapes
// embedded
pc.TrySkipWhiteSpace();
pc.Expecting(); // expect anything but end of input
pc.ClearCapture();
if ('\"' == pc.Current)
{
pc.Capture();
pc.Advance();
// reads until it finds a quote
// using \ as an escape character
// and consuming the final quote
// at the end
pc.TryReadUntil('\"', '\\', true);
// return what we read
return pc.GetCapture();
}
pc.TryReadUntil(false, ',', '}', ']', ' ', '\t', '\r', '\n', '\v', '\f');
return pc.GetCapture();
}
您可以看到,首先,有一些API函数,如TrySkipWhiteSpace()
。这些都在LexContext.BaseExtensions.cs中。它们的作用就是驱动LexContext
以完成所请求的操作,例如跳过空白字符。TrySkipUntil()
/TryReadUntil()
非常有用,因为它们可以识别转义。在上面,我们使用TryReadUntil()
捕获了一个以\
作为转义字符的字符串。
在我们完成了JSON对象到相应对象的解析后,我们只是将它们写入控制台,如下所示
static void _WriteJsonTo(object json, TextWriter writer)
{
var d = json as IDictionary<string, object>;
if (null != d)
_WriteJsonObjectTo(d, writer);
else
{
var l = json as IList<object>;
if (null != l)
_WriteJsonArrayTo(l, writer);
else
writer.Write(json);
}
}
static void _WriteJsonObjectTo(IDictionary<string, object> json, TextWriter writer)
{
var delim = "{";
foreach (var field in json)
{
writer.Write(delim);
_WriteJsonTo(field.Key, writer);
writer.Write(":");
_WriteJsonTo(field.Value, writer);
delim = ",";
}
if ("{" == delim)
writer.Write(delim);
writer.Write("}");
}
static void _WriteJsonArrayTo(IList<object> json, TextWriter writer)
{
var delim = "[";
foreach (var item in json)
{
writer.Write(delim);
_WriteJsonTo(item, writer);
delim = ",";
}
if ("[" == delim)
writer.Write(delim);
writer.Write("]");
}
请注意,这适用于最小化,但它不是符合标准的JSON解析器。为了在演示中保持简单,它比JSON规范“宽松”一些,因为它允许比JSON规范更多的内容。namely,它不强制执行有效的字符串转义,接受更多种类的空白字符,并且不强制字符串在换行符处终止。尽管如此,只要JSON本身是有效的,这个程序就可以解析它。
历史
- 2020年1月17日 - 初始提交