日本語版
最新ニュース
科学&テクノロジー

トークン化と解析に Rust が好きな理由 |めちゃくちゃ

現在、SQL 用の分析ツールを作成しています。 sqleibniz、特に sqlite 方言の場合。目標は、構文チェック、テーブル、列、関数が存在するかどうかのチェックなど、SQL 入力の静的分析を実行することです。これを組み込みの SQLite ランタイムとこのランタイムで条件をアサートする機能と組み合わせることで、SQL の非常に優れた開発エクスペリエンスが作成されます。さらに、コンテキスト、説明、および特定の診断をミュートする機能を備えた高品質のエラー メッセージをユーザーに表示できるようにしたいと考えています。この分析には、字句分析/トークン化の段階、sqlite ドキュメントに従った SQL の解析が含まれます。 そして得られた構築物の分析。プロジェクトの静的解析部分が完了したら、SQL 用の LSP サーバーを作成する予定なので、楽しみにしていてください。上記のプロセスでは、どちらも SQL 用のトークナイザーとパーサーを作成する必要があります。私は sqleibniz の完成には程遠いですが、Rust と、このソフトウェアを開発するためにこの言語が提供する便利な機能に関していくつかの発見をしました。マクロマクロはほとんどの言語で動作が異なります。ただし、これらはコードの重複排除と繰り返しの削減というほぼ同じ理由で使用されます。抽象構文ツリーのノードステートメントのノード sqleibniz 実装は次のように定義されます。1 2#[derive(Debug)] 3/// holds all literal types, such as strings, numbers, etc. 4pub struct Literal…

トークン化と解析に Rust が好きな理由 |めちゃくちゃ

1731056387
2024-11-08 02:35:00

現在、SQL 用の分析ツールを作成しています。 sqleibniz、特に sqlite 方言の場合。

目標は、構文チェック、テーブル、列、関数が存在するかどうかのチェックなど、SQL 入力の静的分析を実行することです。これを組み込みの SQLite ランタイムとこのランタイムで条件をアサートする機能と組み合わせることで、SQL の非常に優れた開発エクスペリエンスが作成されます。

さらに、コンテキスト、説明、および特定の診断をミュートする機能を備えた高品質のエラー メッセージをユーザーに表示できるようにしたいと考えています。

この分析には、字句分析/トークン化の段階、sqlite ドキュメントに従った SQL の解析が含まれます。 そして得られた構築物の分析。

プロジェクトの静的解析部分が完了したら、SQL 用の LSP サーバーを作成する予定なので、楽しみにしていてください。

上記のプロセスでは、どちらも SQL 用のトークナイザーとパーサーを作成する必要があります。私は sqleibniz の完成には程遠いですが、Rust と、このソフトウェアを開発するためにこの言語が提供する便利な機能に関していくつかの発見をしました。

マクロ

マクロはほとんどの言語で動作が異なります。ただし、これらはコードの重複排除と繰り返しの削減というほぼ同じ理由で使用されます。

抽象構文ツリーのノード

ステートメントのノード sqleibniz 実装は次のように定義されます。

1
2#[derive(Debug)]
3/// holds all literal types, such as strings, numbers, etc.
4pub struct Literal {
5    pub t: Token,
6}

さらに、すべてのノードは以下を実装する必要があります。 Node-trait、この特性はすべてのパーサー関数によって返され、後でステートメントの内容を分析するために使用されます。

1pub trait Node: std::fmt::Debug {
2    fn token(&self) -> &Token;
3}

コードの重複

したがって、すべてのノードを定義するだけでなく、その実装も定義する必要があります。
Node-trait を書く必要があります。これには多くのコードの重複が必要ですが、Rust にはその解決策があります。

次のことができるマクロが必要です。

  • 指定された識別子とドキュメントコメントを使用して構造体を定義します
  • 構造体に任意のフィールドを追加する
  • を満たす Node 実装による特性 fn token(&self) -> &Token

マクロで生成する必要がある完全なコードを見てみましょう。
Literal そして Explain ノード。最初のフィールドには、 Token 分野 t、2 番目のノードには、タイプを持つ子フィールドが必要です。

 1#[derive(Debug)]
 2/// holds all literal types, such as strings, numbers, etc.
 3pub struct Literal {
 4    /// predefined for all structures defined with the node! macro
 5    pub t: Token,
 6}
 7impl Node for Literal {
 8    fn token(&self) -> &Token {
 9        &self.t
10    }
11}
12
13
14#[derive(Debug)]
15/// Explain stmt, see: https://www.sqlite.org/lang_explain.html
16pub struct Explain {
17    /// predefined for all structures defined with the node! macro
18    pub t: Token,
19    pub child: OptionBoxdyn Node>>,
20}
21impl Node for Explain {
22    fn token(&self) -> &Token {
23        &self.t
24    }
25}

上記を次の 2 つの呼び出しから生成したいとします。

1node!(
2    Literal,
3    "holds all literal types, such as strings, numbers, etc.",
4);
5node!(
6    Explain,
7    "Explain stmt, see: https://www.sqlite.org/lang_explain.html",
8    child: OptionBoxdyn Node>>,
9);

マクロを使用したコードの重複排除

Rust マクロのドキュメントがそれほど良くないとしても、そのマクロは非常に簡単です。

 1macro_rules! node {
 2    ($node_name:ident,$documentation:literal,$($field_name:ident:$field_type:ty),*) => {
 3        #[derive(Debug)]
 4        #[doc = $documentation]
 5        pub struct $node_name {
 6            /// predefined for all structures defined with the node! macro, holds the token of the ast node
 7            pub t: Token,
 8            $(
 9                pub $field_name: $field_type,
10            )*
11        }
12        impl Node for $node_name {
13            fn token(&self) -> &Token {
14                &self.t
15            }
16        }
17    };
18}

このマクロを詳しく見てみましょう。マクロ引数/メタ変数の定義は次で始まります。
$node_name:ident,$documentation:literal:

 1$node_name : ident , $documentation : literal
 2^^^^^^^^^^ ^ ^^^^^ ^
 3|          | |     |
 4|          | |     metavariable delimiter
 5|          | |
 6|          | metavariable type
 7|          |
 8|          metavariable type delimiter
 9|
10metavariable name

つまり、マクロの最初のメタ変数を Rust が受け入れる有効な識別子として定義し、2 番目の引数をリテラルとして定義します。リテラルは、文字、文字列、生の文字列などのリテラル式を指します。

理解するのに時間がかかった難しい部分は、マクロでメタ変数の繰り返しを定義する方法です。 $($field_name:ident:$field_type:ty),*

1$($field_name:ident:$field_type:ty),*
2^^                 ^              ^ ^
3|                  |              | | 
4|                  metavariable   | repetition  
5|                  delimiter      | (any) 
6|                                 | 
7     sub group of metavariables

私の理解では、メタ変数の定義でサブグループを定義し、その繰り返しを後置します。私たちが使用するのは : メタ変数のサブグループ内で区切るために、これにより便利な形式でマクロを書くことができます。 field_name: type 方法:

1node!(
2    Example,
3    "Example docs", 
4
5    // sub group start
6    field_name: &'static str,
7    field_name1: String
8    // sub group end
9);

を使用できます。 $(...)* サブグループ化されたメタ変数を「ループ」する構文により、それぞれの名前と型を持つすべてのフィールドが作成されます。

1pub struct $node_name {
2    pub t: Token,
3    $(
4        pub $field_name: $field_type,
5    )*
6}

ヒント

見る
繰り返し
メタ変数の繰り返しに関するドキュメント。

覚えておいてください: $documentation メタ変数は、ノードに対して生成したいドキュメント文字列を含むリテラルを保持します。ここでは、 #[doc = ...]
一般に知られている注釈の代わりに /// ... マクロ メタ変数をコンパイラに渡すための構文:

1#[doc = $documentation]
2pub struct $node_name {
3    // ...
4}

各ノードの特性の実装は非常に自明だと思います。

テスト

まず言っておきます: 私はテーブル駆動テストと Go でのテストの記述方法が大好きです:

 1func TestLexerWhitespace(t *testing.T) {
 2    cases := []string{"","t", "rn", " "}
 3    for _, c := range cases {
 4        t.Run(c, func (t *testing.T) {
 5            l := Lexer{}
 6            l.init(c)
 7            l.run()
 8        })
 9    }
10}

Go では、ケースの配列を定義し、ケースごとにテスト関数を実行するだけです c。私の知る限り、Rust は同様のテスト方法を提供していません。そこで、テスト方法を作成しました 😼。

レクサー / トークナイザー テスト

 1#[cfg(test)]
 2mod should_pass {
 3    test_group_pass_assert! {
 4        string,
 5        string: "'text'"=vec![Type::String(String::from("text"))],
 6        empty_string: "''"=vec![Type::String(String::from(""))],
 7        string_with_ending: "'str';"=vec![Type::String(String::from("str")), Type::Semicolon]
 8    }
 9
10    // ...
11}
12
13#[cfg(test)]
14mod should_fail {
15    test_group_fail! {
16        empty_input,
17        empty: "",
18        empty_with_escaped: "\",
19        empty_with_space: " tnr"
20    }
21
22    // ...
23}

これらを実行すると、 cargo test、私が大好きな Go のテーブル駆動テストと同じ出力が得られ、各関数には独自のログとフィードバックがあります (ok/fail):

 1running 68 tests
 2test lexer::tests::should_pass::string::empty_string ... ok
 3test lexer::tests::should_pass::string::string ... ok
 4test lexer::tests::should_pass::string::string_with_ending ... ok
 5test lexer::tests::should_fail::empty_input::empty ... ok
 6test lexer::tests::should_fail::empty_input::empty_with_escaped ... ok
 7test lexer::tests::should_fail::empty_input::empty_with_space ... ok
 8
 9test result: ok. 68 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; 
10finished in 0.00s

マクロは、テスト グループの名前を受け入れます。次に例を示します。 booleans そして
string 入力と予想される出力のペアのリスト。入力は Lexer の初期化と出力 Lexer.run() 期待される出力と比較されます。インライン化 test_group_pass_assert! 呼びかける
string 結果は以下のコードになります。結果のトークン タイプと予想されるトークン タイプが等しいことを主張する前に、変換が必要です。トークン ベクトルをマッピングして、それらのタイプのみを返します。

 1mod string {
 2    use crate::{lexer, types::Type};
 3
 4    #[test]
 5    fn string() {
 6        let input = "'text'".as_bytes().to_vec();
 7        let mut l = lexer::Lexer::new(&input, "lexer_tests_pass");
 8        let toks = l.run();
 9        assert_eq!(l.errors.len(), 0);
10        assert_eq!(
11            toks.into_iter().map(|tok| tok.ttype).collect::VecType>>(),
12            (vec![Type::String(String::from("text"))])
13        );
14    }
15
16    #[test]
17    fn empty_string() {
18        let input = "''".as_bytes().to_vec();
19        let mut l = lexer::Lexer::new(&input, "lexer_tests_pass");
20        let toks = l.run();
21        assert_eq!(l.errors.len(), 0);
22        assert_eq!(
23            toks.into_iter().map(|tok| tok.ttype).collect::VecType>>(),
24            (vec![Type::String(String::from(""))])
25        );
26    }
27
28    #[test]
29    fn string_with_ending() {
30        let input = "'str';".as_bytes().to_vec();
31        let mut l = lexer::Lexer::new(&input, "lexer_tests_pass");
32        let toks = l.run();
33        assert_eq!(l.errors.len(), 0);
34        assert_eq!(
35            toks.into_iter().map(|tok| tok.ttype).collect::VecType>>(),
36            (vec![Type::String(String::from("str")), Type::Semicolon])
37        );
38    }
39}

カウンター部分 test_group_fail! のために empty_input! 以下のコードが生成されます。主な違いは、結果のトークン ベクトルが空であると主張することと、 Lexer.errors フィールドに少なくとも 1 つのエラーが含まれる必要があります。

 1mod empty_input {
 2    use crate::lexer;
 3
 4    #[test]
 5    fn empty() {
 6        let source = "".as_bytes().to_vec();
 7        let mut l = lexer::Lexer::new(&source, "lexer_tests_fail");
 8        let toks = l.run();
 9        assert_eq!(toks.len(), 0);
10        assert_ne!(l.errors.len(), 0);
11    }
12
13    #[test]
14    fn empty_with_escaped() {
15        let source = "\".as_bytes().to_vec();
16        let mut l = lexer::Lexer::new(&source, "lexer_tests_fail");
17        let toks = l.run();
18        assert_eq!(toks.len(), 0);
19        assert_ne!(l.errors.len(), 0);
20    }
21
22    #[test]
23    fn empty_with_space() {
24        let source = " tnr".as_bytes().to_vec();
25        let mut l = lexer::Lexer::new(&source, "lexer_tests_fail");
26        let toks = l.run();
27        assert_eq!(toks.len(), 0);
28        assert_ne!(l.errors.len(), 0);
29    }
30}

マクロ自体を見てみましょう。マクロ定義については詳しく説明しません。なぜなら、メタ変数の宣言については前に説明したからです。 。最初のマクロは、有効な入力を使用した test のアサーションに使用されます。 test_group_pass_assert!:

 1macro_rules! test_group_pass_assert {
 2    ($group_name:ident,$($ident:ident:$input:literal=$expected:expr),*) => {
 3    mod $group_name {
 4        use crate::{lexer, types::Type};
 5
 6        $(
 7            #[test]
 8            fn $ident() {
 9                let input = $input.as_bytes().to_vec();
10                let mut l = lexer::Lexer::new(&input, "lexer_tests_pass");
11                let toks = l.run();
12                assert_eq!(l.errors.len(), 0);
13                assert_eq!(toks.into_iter().map(|tok| tok.ttype).collect::VecType>>(), $expected);
14            }
15        )*
16        }
17    };
18}

2 番目は、無効な入力と予想されるエラーを伴うエッジケースのテストに使用されます。 test_group_fail!:

 1macro_rules! test_group_fail {
 2    ($group_name:ident,$($name:ident:$value:literal),*) => {
 3        mod $group_name {
 4        use crate::lexer;
 5        $(
 6            #[test]
 7            fn $name() {
 8                let source = $value.as_bytes().to_vec();
 9                let mut l = lexer::Lexer::new(&source, "lexer_tests_fail");
10                let toks = l.run();
11                assert_eq!(toks.len(), 0);
12                assert_ne!(l.errors.len(), 0);
13            }
14         )*
15        }
16    };
17}

パーサーテスト

同じ概念とほぼ同じマクロを使用します。 parser パーサーが生成する結果をテストするモジュールですが、今回はエッジケースと完全な SQL ステートメントに焦点を当てています。たとえば、次の場合に合格と失敗が予想されるテストです。 EXPLAIN SQL ステートメント:

 1#[cfg(test)]
 2mod should_pass {
 3    test_group_pass_assert! {
 4        sql_stmt_prefix,
 5        explain: r#"EXPLAIN VACUUM;"#=vec![Type::Keyword(Keyword::EXPLAIN)],
 6        explain_query_plan: r#"EXPLAIN QUERY PLAN VACUUM;"#=vec![Type::Keyword(Keyword::EXPLAIN)]
 7    }
 8}
 9
10#[cfg(test)]
11mod should_fail {
12    test_group_fail! {
13        sql_stmt_prefix,
14        explain: r#"EXPLAIN;"#,
15        explain_query_plan: r#"EXPLAIN QUERY PLAN;"#
16    }
17}

両方のマクロが取得するのは、 sql_stmt_prefix モジュール名として使用します。これは、パーサー内で、 EXPLAIN 声明。失敗したテストでは、パーサーが SQL 標準で規定されている条件を正しくアサートしているかどうかをチェックします。 sqlite – SQL-stmt。具体的には、次のいずれかのステートメントが続きます。 EXPLAIN 識別子または QUERY PLAN そして声明が続きます。

これらのテストとレクサーのテストの違いは、アサーションの作成方法にあります。マクロが生成するコードを見てください。

 1#[cfg(test)]
 2mod should_pass {
 3    mod sql_stmt_prefix {
 4        use crate::{lexer, parser::Parser, types::Keyword, types::Type};
 5
 6        #[test]
 7        fn explain() {
 8            let input = r#"EXPLAIN VACUUM;"#.as_bytes().to_vec();
 9            let mut l = lexer::Lexer::new(&input, "parser_test_pass");
10            let toks = l.run();
11            assert_eq!(l.errors.len(), 0);
12            let mut parser = Parser::new(toks, "parser_test_pass");
13            let ast = parser.parse();
14            assert_eq!(parser.errors.len(), 0);
15            assert_eq!(
16                ast.into_iter()
17                    .map(|o| o.unwrap().token().ttype.clone())
18                    .collect::VecType>>(),
19                (vec![Type::Keyword(Keyword::EXPLAIN)])
20            );
21        }
22
23        #[test]
24        fn explain_query_plan() {
25            let input = r#"EXPLAIN QUERY PLAN VACUUM;"#.as_bytes().to_vec();
26            let mut l = lexer::Lexer::new(&input, "parser_test_pass");
27            let toks = l.run();
28            assert_eq!(l.errors.len(), 0);
29            let mut parser = Parser::new(toks, "parser_test_pass");
30            let ast = parser.parse();
31            assert_eq!(parser.errors.len(), 0);
32            assert_eq!(
33                ast.into_iter()
34                    .map(|o| o.unwrap().token().ttype.clone())
35                    .collect::VecType>>(),
36                (vec![Type::Keyword(Keyword::EXPLAIN)])
37            );
38        }
39    }
40}

示されているように、 test_group_pass_assert! のマクロ parser モジュールは同じで始まります Lexer 初期化と空のエラー ベクトル アサーション。ただし、次のステップでは、 Parser 構造を解析し、解析後に結果をアサートします。つまり、エラーがなく、ノードが正しいタイプであることを確認します。

 1#[cfg(test)]
 2mod should_fail {
 3    mod sql_stmt_prefix {
 4        use crate::{lexer, parser::Parser};
 5        #[test]
 6        fn explain() {
 7            let input = r#"EXPLAIN;"#.as_bytes().to_vec();
 8            let mut l = lexer::Lexer::new(&input, "parser_test_fail");
 9            let toks = l.run();
10            assert_eq!(l.errors.len(), 0);
11            let mut parser = Parser::new(toks, "parser_test_fail");
12            let _ = parser.parse();
13            assert_ne!(parser.errors.len(), 0);
14        }
15
16        #[test]
17        fn explain_query_plan() {
18            let input = r#"EXPLAIN QUERY PLAN;"#.as_bytes().to_vec();
19            let mut l = lexer::Lexer::new(&input, "parser_test_fail");
20            let toks = l.run();
21            assert_eq!(l.errors.len(), 0);
22            let mut parser = Parser::new(toks, "parser_test_fail");
23            let _ = parser.parse();
24            assert_ne!(parser.errors.len(), 0);
25        }
26    }
27}

test_group_fail! マクロも同じマクロを拡張します。 lexer
モジュールを作成し、解析後にエラーのチェックを追加します。両方 macro_rules!:

 1macro_rules! test_group_pass_assert {
 2    ($group_name:ident,$($ident:ident:$input:literal=$expected:expr),*) => {
 3    mod $group_name {
 4        use crate::{lexer, parser::Parser, types::Type, types::Keyword};
 5        $(
 6            #[test]
 7            fn $ident() {
 8                let input = $input.as_bytes().to_vec();
 9                let mut l = lexer::Lexer::new(&input, "parser_test_pass");
10                let toks = l.run();
11                assert_eq!(l.errors.len(), 0);
12
13                let mut parser = Parser::new(toks, "parser_test_pass");
14                let ast = parser.parse();
15                assert_eq!(parser.errors.len(), 0);
16                assert_eq!(ast.into_iter()
17                    .map(|o| o.unwrap().token().ttype.clone())
18                    .collect::VecType>>(), $expected);
19            }
20        )*
21        }
22    };
23}
24
25macro_rules! test_group_fail {
26    ($group_name:ident,$($ident:ident:$input:literal),*) => {
27    mod $group_name {
28        use crate::{lexer, parser::Parser};
29        $(
30            #[test]
31            fn $ident() {
32                let input = $input.as_bytes().to_vec();
33                let mut l = lexer::Lexer::new(&input, "parser_test_fail");
34                let toks = l.run();
35                assert_eq!(l.errors.len(), 0);
36
37                let mut parser = Parser::new(toks, "parser_test_fail");
38                let _ = parser.parse();
39                assert_ne!(parser.errors.len(), 0);
40            }
41        )*
42        }
43    };
44}

マクロの落とし穴

  • rust-analyzer 中ではひどい遊びをする macro_rules!
    • 本当のインテリセンスはない
    • goto定義はありません
    • リテラルおよび言語構成体の署名にはホバーはありません
  • cargo fmt 内部で書式設定またはインデントを行わない macro_rules! およびマクロ呼び出し
  • treesitter (はい、ちなみに私は neovim を使っています 😼) chroma (このサイトで使用されています) の構文の強調表示に時々苦労することがあります。 macro_rules!
  • ドキュメントはせいぜい少ない

一致する文字

レクサーを作成する場合、文字の比較は他のすべてが依存する部分です。 Rust では、これを次の方法で楽しむことができます。 matches! マクロとパターン
match ステートメントは受け入れます。たとえば、現在の文字が有効な sqlite 数値であるかどうかを確認するには、次の単純な方法を使用します。 matches! マクロの呼び出し:

 1/// Specifically matches https://www.sqlite.org/syntax/numeric-literal.html
 2fn is_sqlite_num(&self) -> bool {
 3    matches!(self.cur(), 
 4             // exponent notation with +-
 5             '+' | '-' |
 6             // sqlite allows for separating numbers by _
 7             '_' |
 8             // floating point
 9             '.' |
10             // hexadecimal
11             'a'..='f' | 'A'..='F' |
12             // decimal
13             '0'..='9')
14}

同様に、識別子のテストも上記と同じくらい簡単です。

1fn is_ident(&self, c: char) -> bool {
2    matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '0'..='9')
3}

レクサーのメインループでのシンボル検出はまったく同じように機能します。

 1pub fn run(&mut self) -> VecToken> {
 2    let mut r = vec![];
 3    while !self.is_eof() {
 4        match self.cur() {
 5            // skipping whitespace
 6            't' | 'r' | ' ' | 'n' => {}
 7            '*' => r.push(self.single(Type::Asteriks)),
 8            ';' => r.push(self.single(Type::Semicolon)),
 9            ',' => r.push(self.single(Type::Comma)),
10            '%' => r.push(self.single(Type::Percent)),
11            _ => {
12                // omitted error handling for unknown symbols
13                panic!("whoops");
14            } 
15        }
16        self.advance();
17    }
18    r
19}

のパターン match 声明と matches ブロックはおそらく Rust の最も便利な機能です。

一致するトークン

レクサーが文字ストリームを次のストリームに変換すると、 Token 位置情報と型情報を持つ構造体インスタンスを使用すると、パーサーはこのストリームを使用して、抽象構文ツリーを生成できます。パーサーは、トークン タイプを検出して入力内のパターンを認識する必要があります。これもまた錆びるケースです。 match という発言が光ります。

それぞれ Token が含まれています t そのタイプについてはフィールドを参照してください。以下を参照してください。

 1pub use self::keyword::Keyword;
 2
 3#[derive(Debug, PartialEq, Clone)]
 4pub enum Type {
 5    Keyword(keyword::Keyword),
 6    Ident(String),
 7    Number(f64),
 8    String(String),
 9    Blob(Vecu8>),
10    Boolean(bool),
11    ParamName(String),
12    Param(usize),
13
14    Dot,
15    Asteriks,
16    Semicolon,
17    Percent,
18    Comma,
19
20    Eof,
21}

見てみましょう sql_stmt_prefix パーサーのメソッド。この関数は、 EXPLAIN このステートメントは、sqlite のドキュメントによると、他のすべての SQL ステートメントの前に付けられるため、この名前が付けられています。対応する構文図を以下に示します。

実装はこの図に従います。の Explain stmt はオプションであるため、現在のトークン タイプが一致しない場合は Type::Keyword(Keyword::EXPLAIN)と呼びます。 sql_stmt 構文図の右側のステートメントを処理する関数。

トークンが一致すると、トークンが消費され、次のチェックは、 EXPLAIN 図: QUERY PLAN。これには両方が必要です
QUERY そして PLAN キーワードを連続して入力すると、両方とも消費されます。

 1impl'a> Parser'a> {
 2    fn sql_stmt_prefix(&mut self) -> OptionBoxdyn Node>> {
 3        match self.cur()?.ttype {
 4            Type::Keyword(Keyword::EXPLAIN) => {
 5                let mut e = Explain {
 6                    t: self.cur()?.clone(),
 7                    child: None,
 8                };
 9                self.advance(); // skip EXPLAIN
10
11                // path for EXPLAIN->QUERY->PLAN
12                if self.is(Type::Keyword(Keyword::QUERY)) {
13                    self.consume(Type::Keyword(Keyword::QUERY));
14                    self.consume(Type::Keyword(Keyword::PLAN));
15                } // else path is EXPLAIN->*_stmt
16
17                e.child = self.sql_stmt();
18                Some(Box::new(e))
19            }
20            _ => self.sql_stmt(),
21        }
22    }
23}

これは、パーサーでのパターン マッチングの基本的な使用法を示しています。他の例としては、 literal_value 関数の唯一の目的は、
Literal すべてのリテラルのノード。

リテラル値の構文図

ほとんどの埋め込み列挙値は破棄されますが、いくつかの特定のキーワードはリテラルでありながらキーワードとみなされているため、チェックされます。

 1impl'a> Parser'a> {
 2    /// see: https://www.sqlite.org/syntax/literal-value.html
 3    fn literal_value(&mut self) -> OptionBoxdyn Node>> {
 4        let cur = self.cur()?;
 5        match cur.ttype {
 6            Type::String(_)
 7            | Type::Number(_)
 8            | Type::Blob(_)
 9            | Type::Keyword(Keyword::NULL)
10            | Type::Boolean(_)
11            | Type::Keyword(Keyword::CURRENT_TIME)
12            | Type::Keyword(Keyword::CURRENT_DATE)
13            | Type::Keyword(Keyword::CURRENT_TIMESTAMP) => {
14                let s: OptionBoxdyn Node>> = Some(Box::new(Literal { t: cur.clone() }));
15                self.advance();
16                s
17            }
18            _ => {
19                // omitted error handling for invalid literals
20                panic!("whoops");
21            }
22        }
23    }
24}

派手なエラー表示

実装自体は反復的でそれほど興味深いものではありませんが、レクサーとパーサーの両方がエラーを処理する方法と、これらのエラーがユーザーにどのように表示されるかを紹介したいと思いました。典型的なエラーは、SQL ステートメントの末尾のセミコロンを見逃すことです。

1-- ./vacuum.sql
2-- rebuilding the database into a new file
3VACUUM INTO 'optimized.db'

このファイルを渡すのは、 sqleibniz すぐにエラーが発生します:

真空エラー

オプション

Rust のエラー処理は楽しいものであり、 ?-演算子は理にかなっています。しかし、Rust はさらに進んでおり、単に内部の値を変更できるだけではありません。 Option 存在する場合は、条件を確認したり、デフォルト値を指定したりすることもできます。

あるあると

場合によっては、入力ストリームの次の文字が利用可能で述語を渡すかどうかを確認するだけで済みます。 is_some_and この理由のために存在します:

1fn next_is(&mut self, c: char) -> bool {
2    self.source
3        .get(self.pos + 1)
4        .is_some_and(|cc| *cc == c as u8)
5}
6
7fn is(&self, c: char) -> bool {
8    self.source.get(self.pos).is_some_and(|cc| *cc as char == c)
9}

上記は非常に読みやすいですが、以下はそれほど読み物ではありません。

 1fn next_is(&mut self, c: char) -> bool {
 2    match self.source.get(self.pos + 1) {
 3        Some(cc) => *cc == c as u8,
 4        _ => false,
 5    }
 6}
 7
 8fn is(&self, c: char) -> bool {
 9    match self.source.get(self.pos) {
10        Some(cc) => *cc as char == c,
11        _ => false,
12    }
13}

地図

入力は次のベクトルであるため、 u8のベクトルではありません char、この変換は次のように行われます。 map:

1fn next(&self) -> Optionchar> {
2    self.source.get(self.pos + 1).map(|c| *c as char)
3}

更新された値をラップ解除して再ラップする代わりに、次のようにします。

1fn next(&self) -> Optionchar> {
2    match self.source.get(self.pos + 1) {
3        Some(c) => Some(*c as char),
4        _ => None,
5    }
6}

地図または

同様の方法で、sqleibniz パーサーは次を使用します。 map_or 現在のトークンが次の場合に限り、タイプのチェックを返します。 Some:

1fn next_is(&self, t: Type) -> bool {
2    self.tokens
3        .get(self.pos + 1)
4        .map_or(false, |tok| tok.ttype == t)
5}
6
7fn is(&self, t: Type) -> bool {
8    self.cur().map_or(false, |tok| tok.ttype == t)
9}

繰り返しますが、それほど慣用的ではない解決策を置き換えます。

 1fn next_is(&self, t: Type) -> bool {
 2    match self.tokens.get(self.pos + 1) {
 3        None => false,
 4        Some(token) => token.ttype == t,
 5    }
 6}
 7
 8fn is(&self, t: Type) -> bool {
 9    if let Some(tt) = self.cur() {
10        return tt.ttype == t;
11    }
12    false
13}

イテレータ 💖

文字のフィルタリング

Rust の数値解析は許可されていません _、sqlite 数値解析は受け入れます _したがって、レクサーもこれらの文字を消費しますが、Rust 番号解析ロジックを介して入力を解析する前にこれらの文字をフィルタリングします。

1let str = self
2    .source
3    .get(start..self.pos)
4    .unwrap_or_default()
5    .iter()
6    .map(|c| *c as char)
7    .filter(|c| *c != '_')
8    .collect::String>();

ヒント

使用すべきではないことはわかっています unwrap ただし、この状況では、パーサーはいずれにしても空の文字列を有効な数値として受け入れないため、デフォルト値ではいずれにしても失敗します。

Goでは、最初にforループで文字リストを反復し、各バイトを文字列バッファに書き込む必要があります(各書き込みはところで失敗する可能性がありますが、少なくとも error)その後、を作成する必要があります string からの strings.Builder 構造。

1s := source[start:l.pos]
2b := strings.Builder{}
3b.Grow(len(s))
4for _, c := range s {
5    if c != '_' {
6        b.WriteByte(c)
7    }
8}
9s = b.String()

文字を確認する

Sqlite は 16 進データを BLOB として受け入れます。 x''、入力が正しいことを確認するには、この配列内のすべての文字が有効な 16 進数であることを確認する必要があります。さらに、エラーを正しく表示するには位置情報が必要です。このために、
self.string() メソッドを使用し、 chars() イテレータ作成関数と enumerate 関数。

 1if let Ok(str_tok) = self.string() {
 2    if let Type::String(str) = &str_tok.ttype {
 3        let mut had_bad_hex = false;
 4        for (idx, c) in str.chars().enumerate() {
 5            if !c.is_ascii_hexdigit() {
 6                // error creation and so on omitted here 
 7                had_bad_hex = true;
 8                break;
 9            }
10        }
11        if had_bad_hex {
12            break;
13        }
14
15        // valid hexadecimal data in blob
16    }
17} else {
18    // error handling omitted
19}

BLOB 内に無効な文字が見つかった場合、エラー表示では次のエラーが生成されます。

無効な BLOB データ

情報

ここまで読んでいただきありがとうございます😼。

(技術的またはセマンティックな) エラーを見つけた場合は、次のアドレスに正しい方向への指示を電子メールで送ってください。 [email protected]
([email protected])。

#トークン化と解析に #Rust #が好きな理由 #めちゃくちゃ

執筆者について: nipponese

Nipponese News編集部は、国内外のニュースを日本語で分かりやすくお届けします。