写点什么

语法分析 开端

作者:Miracle
  • 2025-09-23
    四川
  • 本文字数:3119 字

    阅读完需:约 10 分钟

语法分析 开端

传统的程序设计语言里面,在语法分析之前还有一个阶段叫词法分析,就是说把所有的文字的输输入按照合理的分解为 token。 但是随着这个工程的这种实践,大家发现这个阶段没什么用,因为它很短,然后只要是我们在这个设计分析的时候,这个做的足够合理,这个步骤可以和语法分析合在一起,所以我们不单独一个词法分析的步骤。


那么 rust 世界里面有一个比较有名的语法分析的库叫 nom。他是采用他们的思路是采用这种叫 combination,就是组合的方式。它是通过很小很小的一个个你可以认为它是很简单的一个。这是特定语法符号的一个小的算子,然后通过 and or choice and then 这种所谓的介词,把自己小的这种这种算子组合起来 形成 新的算子,

然后具体的算子以各种这样组合起来更复杂的更大的一个算子。最终我们的 program 通过一个算子就可以解析。所以它是一种自下而上的构造的逻辑。这个是一个非常有非常合理的。因为你随时可以看到每一步的进展。所以这是我们需要采用的一个工具。Rust 官方的那个叫做 LSP,大概用 VS cod e 里面可能或者用其他的这种 IDE。那个叫语言服务器,它需要快速的增量式的代码进行解析,所以它应该说他在开发更现代化,更高效的这种。

语法这个过去叫做乔姆斯基,就是一个大的语言学家。Chumsky 的方法就是它的整个逻辑的思路跟相似的。And


好,让我们从头开始,那么所有的编程语言一般给你的第一句话就是 hello world。 print Hello world

是一个很好的开始点。但是对于我们要想从头设计一门语言的这种相对比较专业的资深的不够,而且一点都不具有典型性。所以我们从一个呃更加能够表达一个专业社区的专业码农的这样一个构建一个编辑或者构建英文语言的这个开始。一定是什么呢?让我们看看啊,是下面这两行代码。


let name = "zhuchuanjing";let age = 10;
复制代码

这两行代码从这里学过编程的人应该一眼就能看出来。它是两个赋值语句,其实你可以把它看成一个。

但是 我们为了说明一下字符串和数字的区别,他就是一个非常典型的,比如说我们在设计一个语言的时候,

设计为新语言才是一个真正有价值的出发点,这个比什么 print hello,要强很多的。

我们一个个来看。第一个 就是按照按照那个语言学的概念来说。行为与语义来说,它是将一个值赋给一个名字。但是从指称语义的角度看来讲,它是给一个值一个命名。这两个有一些微妙的区别,但是我们现在暂时不去区分它了,我们就从因为咱的科学更多的是从使用使用操作语义上来讲,所以我们认为就是说他是给一个值。赋值给一个名字。

那么我们如果要设计一个解析器。两个层面,

第一个层面,我们能够解析两个最基础的语法单元。一个是所谓的名字,那么我们的专业名词叫做就是一个名称标识。第二个呢是后面的那个字符串。或者是一个 10 这个叫文本量,

它就是通过文本去赋给了一个具体的特定的值,就叫文本量。

赋值的一个动作。所以我们我们的这个 Parser 首先要能够解析三个东西,能够解析关键字。能够这个写的文本量。一个是字符串,一个是数字。

那么这个这个单词我们等会儿再去赋值语句的时候,直接让我们去解释。所以先不管他。那么我们首先要实现两个刚才讲的语法算是一个是文本,但文本的太复杂了。我们下面看那个标识符。

use chumsky::prelude::*;use chumsky::error::Rich;
type Extra<'a> = chumsky::extra::Full<Rich<'a, char>, (), ()>;
复制代码

我们首先做一下准备工作。嗯,上面的代码里面引入了所有相关的解析库的内容,

然后我们定义了一个类型他使用了两个 Trait 参数,Rich 可以是反馈错误。包括完整的代码所在的位置。


const KEYWORDS: &[&str] = &["for", "if", "else", "break", "continue", "fn", "let", "return", "struct", "const", "static"];
pub fn ident_parser<'a>()-> impl Parser<'a, &'a str, SmolStr, Extra<'a>> + Clone { text::ident().try_map(|ident: &str, span| -> Result<SmolStr, Rich<'a, char>> { if KEYWORDS.contains(&ident) { Err(Rich::custom(span, format!("'{ident}' is a keyword"))) } else { Ok(SmolStr::from(ident)) } }).padded().labelled("identifier")}
复制代码

大家看一下。上面的代码呃就是实现了一个标识符解析,所以这种这种就是所谓的组合式解析的方式。就是他的的方式是用这种函数,就是像我的 ident_parser 它就是返回一个实现了这种解析器接口的这个算子,这个算子本身你可以看到它是由系统预定一个算子,比如说这样 text 这个系统预定的算子,然后这个算子的通过我们的这个 map 就是

所谓的 rust 是一个函数设计语言,大家一定要记住。

他把这个这个 result 的东西里面的东西经过我们自己的处理,比如说我们看我们看他是不是这个返回的这个 text 的是不是包含在我们的关键字里面?如果不是我们就返回错误,如果是我们就要把这个的我们自己收藏类型里面把它发布出去。所以这个函数就能够得到一个解析标识符的算子。

啊,下面来一点挑战的。呃,我们现在需要输入一段解析,一段字符串。字符串的话是呃包括在包括在这个两个双引号中间的这样一段。对吧?如果但这样很简单,但是呢我们实际的编程语言往往会面临到有转义符号。什么叫转移符啊?就是 \r \n 表示回车换行。\t 表示制表符。等等等等类似的东西。所以我们的这种字符串解能够需要能够处理这个。代码如下

pub fn string_parser<'a,>() -> impl Parser<'a, &'a str, String, Extra<'a>> + Clone {    let escape = just('\\',).ignore_then(choice((just('"',).to('"',), just('n',).to('\n',), just('r',).to('\r',), just('t',).to('\t',), just('\\',).to('\\',),),),);    let content = escape.or(any().filter(|c| *c != '"' && *c != '\\',),).repeated().collect::<String>();    just('"',).ignore_then(content,).then_ignore(just('"',),)}
复制代码

大家看到这个解析器,它这个函数也是返回一个。这是字符串的算子。准确的是解析字面量字符串的算子。

然后对于那个一般的程序员的时候都要支持注释。那么我们按照 c 语言或者 c 语言的标准方式注释包括这个两个斜杠的单行柱。也就是从两个斜杠开始到一直到行尾都是做事。以及整块的做就是斜杠星号到星号斜杠为止,中间这一段都是按照这两种标准的注释方式,我们来做这个。

fn line_comment<'a,>() -> impl Parser<'a, &'a str, (), Extra<'a>> + Clone { just("//",).ignore_then(any().filter(|c: &char| *c != '\n' && *c != '\r',).repeated(),).then_ignore(text::newline().or(end(),),).ignored() }
fn block_comment<'a,>() -> impl Parser<'a, &'a str, (), Extra<'a>> + Clone { let not_star = any().filter(|c: &char| *c != '*',).ignored(); let star_not_slash = just('*',).then(any().filter(|c: &char| *c != '/',),).ignored(); just("/*",).ignore_then(choice((not_star, star_not_slash,),).repeated(),).then_ignore(just("*/",),).ignored()}
复制代码

在这个语言的解析过程中,所有的空白,包括注释,包括回车换行都是要忽略的。我们把这些叫做语法空白。我们用一个统一的算子来忽略这些空白

pub fn rust_ws<'a,>()-> impl Parser<'a, &'a str, (), Extra<'a>> + Clone { choice((text::whitespace().at_least(1,).ignored(), line_comment(), block_comment(),),).repeated().ignored() }
复制代码

我们还有一些基本的常量要解析 包括 true false 和 null

pub fn const_value_parser<'a,>() -> impl Parser<'a, &'a str, Dynamic, Extra<'a>> + Clone { choice((just("true",).to(Dynamic::from(true)),    just("false",).to(Dynamic::from(false)), just("null",).to(Dynamic::Null))) }
复制代码

这个时候就看到我们之前 Dynamic 的 作用了吧,所有的字面量都可以装到 Dynamic 中

用户头像

Miracle

关注

三十年资深码农 2019-10-25 加入

还未添加个人简介

评论

发布
暂无评论
语法分析 开端_Miracle_InfoQ写作社区