Slide 1

Slide 1 text

@quasilyte 2021 Parsing & Go 1

Slide 2

Slide 2 text

ПАРСИТЬ? Кому вообще это нужно? 2

Slide 3

Slide 3 text

3 Database firewall (SQL parsing) Data Armor Go компилятор и ассемблер Intel NoVerify линтер VK KPHP компилятор VK Всю жизнь парсил, продолжаю парсить и буду парсить

Slide 4

Slide 4 text

4 А ещё был open source regexp-lint go-critic go-ruleguard phpgrep

Slide 5

Slide 5 text

NoVerify (Go) Вручную написанный парсер 5 Генератор FSM Ragel Генератор парсеров goyacc Что из этого мы используем?

Slide 6

Slide 6 text

NoVerify (Go) ✅ Вручную написанный парсер 6 ✅ Генератор FSM Ragel ✅ Генератор парсеров goyacc Всё!

Slide 7

Slide 7 text

KPHP (C++) Вручную написанный парсер 7 Генератор парсеров bison Генератор лексеров lex Что из этого мы используем?

Slide 8

Slide 8 text

KPHP (C++) ✅ Вручную написанный парсер 8 ✅ Генератор парсеров bison ✅ Генератор лексеров lex Всё!

Slide 9

Slide 9 text

История из нашей команды 9 ⌐■_■ ಠ_ಠ ◕‿◕ •◡• Автор NoVerify

Slide 10

Slide 10 text

История из нашей команды 10 ಠ_ಠ ◕‿◕ •◡• Google ■_■¬ Автор NoVerify

Slide 11

Slide 11 text

История из нашей команды 11 ಠ_ಠ ◕‿◕ •◡• NoVerify

Slide 12

Slide 12 text

Что мы будем делать Чего здесь не будет 12 ● Убедимся, что парсинг - не вымышленная задача ● Разберём несколько способов парсинга ● Рассмотрим типичные проблемы и их решения ● Введения в лексический анализ и парсинг ● Подробнейшего разбора какого-то из инструментов ● Академической точности

Slide 13

Slide 13 text

Что мы будем делать Чего здесь не будет 13 ● Убедимся, что парсинг - не вымышленная задача ● Разберём несколько способов парсинга ● Рассмотрим типичные проблемы и их решения ● Введения в лексический анализ и парсинг ● Подробнейшего разбора какого-то из инструментов ● Академической точности ✅

Slide 14

Slide 14 text

Что мы будем делать Чего здесь не будет 14 ● Убедимся, что парсинг - не вымышленная задача ● Разберём несколько способов парсинга ● Рассмотрим типичные проблемы и их решения ● Введения в лексический анализ и парсинг ● Подробнейшего разбора какого-то из инструментов ● Академической точности А ещё будут бенчмарки! ✅

Slide 15

Slide 15 text

А что будем парсить? 15

Slide 16

Slide 16 text

/** * @return ?int|void|string[] */ function check_rights() { return false; } Типы внутри phpdoc комментариев 16

Slide 17

Slide 17 text

А зачем это парсить? 17 phpdoc comment (текст) phpdoc types (AST) парсер сложно анализировать легко анализировать

Slide 18

Slide 18 text

Выражения типов внутри phpdoc 18 int, float, null primitive type Foo, Foo\Bar [qualified] type name ?T nullable type T? optional key type T[] array type X|Y union type X&Y intersection type

Slide 19

Slide 19 text

Выражения типов внутри phpdoc 19 int, float, null primitive type Foo, Foo\Bar [qualified] type name ?T nullable type T? optional key type T[] array type X|Y union type X&Y intersection type Сложна

Slide 20

Slide 20 text

participle 20

Slide 21

Slide 21 text

21 Go structs Inferred parser type Expr struct { Number *float64 `@(Float|Int)` Var *string `| @Ident` } parser := participle.MustBuild(&Expr{})

Slide 22

Slide 22 text

Быстрая справка (но у нас нет на неё времени) 22 @ Парсим expr, кладём в поле @@ Парсим по типу поля, кладём туда же Ident, Int, ... Парсим именованный токен "foo" Парсим токен с ровно таким текстом | Парсим альтернативы (or) * Парсим expr 0-n раз (результат - слайс) ? Парсим expr 0-1 раз

Slide 23

Slide 23 text

type TypeNameExpr struct { Primitive *string `@("int"|"float")` Class *ClassNameExpr `| @@` } type ClassNameExpr struct { Part string `@Ident` Next *ClassNameExpr `("\\" @@)?` } Парсим простые выражения типов 23

Slide 24

Slide 24 text

1. Имена, скобочки (самый высокий) 2. Nullability operator 3. Массивы, optional key operator 4. Intersection 5. Union (самый низкий) Приоритеты операторов 24 X|Y&Z == X|(Y&Z) ?int[] == (?int)[]

Slide 25

Slide 25 text

1. Имена, скобочки (самый высокий) 2. Nullability operator 3. Массивы, optional key operator 4. Intersection 5. Union (самый низкий) Выражаем приоритеты через грамматику 25 X|Y&Z == X|(Y&Z) ?int[] == (?int)[] ● PrimaryExpr ● PrefixExpr ● PostfixExpr ● IntersectionExpr ● UnionExpr

Slide 26

Slide 26 text

1. PrimaryExpr int float Foo (int) 2. PrefixExpr ?X 3. PostfixExpr X? X[] 4. IntersectionExpr X&Y 5. UnionExpr X|Y 26 type PrefixExpr struct { Ops []*PrefixOp `@@*` Right *PrimaryExpr `@@` }

Slide 27

Slide 27 text

1. PrimaryExpr int float Foo (int) 2. PrefixExpr ?X 3. PostfixExpr X? X[] 4. IntersectionExpr X&Y 5. UnionExpr X|Y 27 type PostfixExpr struct { Left *PrefixExpr `@@` Ops []*PostfixOp `@@*` }

Slide 28

Slide 28 text

1. PrimaryExpr int float Foo (int) 2. PrefixExpr ?X 3. PostfixExpr X? X[] 4. IntersectionExpr X&Y 5. UnionExpr X|Y 28 type IntersectionExpr struct { Left *PostfixExpr `@@` Right *IntersectionExpr `("&" @@)?` }

Slide 29

Slide 29 text

1. PrimaryExpr int float Foo (int) 2. PrefixExpr ?X 3. PostfixExpr X? X[] 4. IntersectionExpr X&Y 5. UnionExpr X|Y 29 type UnionExpr struct { Left *IntersectionExpr `@@` Right *UnionExpr `("|" @@)?` }

Slide 30

Slide 30 text

1. PrimaryExpr int float Foo (int) 2. PrefixExpr ?X 3. PostfixExpr X? X[] 4. IntersectionExpr X&Y 5. UnionExpr X|Y 30 type PrimaryExpr struct { TypeName *TypeNameExpr `@@` Parens *UnionExpr `| "(" @@ ")"` }

Slide 31

Slide 31 text

Запустим парсер! 31

Slide 32

Slide 32 text

Запустим парсер! 32 int

Slide 33

Slide 33 text

Запустим парсер! 33 int { "Left": { "Left": { "Left": { "Ops": null, "Right": { "TypeName": { "Primitive": "int", "Class": null }, "Parens": null } }, "Ops": null }, "Right": null }, "Right": null }

Slide 34

Slide 34 text

● Автоматический мапинг грамматики на AST ● Не нужны внешние утилиты типа yacc ● Легко парсить простые форматы ● Может получиться не очень удобное AST ● Нет простых способов работы с приоритетами операторов ● Парсер может получиться медленным Плюсы participle (+) Минусы Participle (-) 34

Slide 35

Slide 35 text

В исходниках vk.com примерно 400000 phpdoc типов Отслеживаем производительность 35 Метод парсинга Парсим Foo|Bar|null 400000 раз participle 78000 наносек ~31 сек

Slide 36

Slide 36 text

Преобразуем AST 36

Slide 37

Slide 37 text

Да! Нет! Будешь использовать сгенерированные под grpc типы в бизнес-логике?

Slide 38

Slide 38 text

Вводим новые AST-типы 38 Оригинальное AST Удобное AST Создано парсером package phpdoc

Slide 39

Slide 39 text

type IntersectionType struct { Left Type Right Type } type UnionType struct { Left Type Right Type } type ClassName struct { Parts []string } type Type interface { String() string } type IntersectionExpr struct { Left *PostfixExpr `@@` Right *IntersectionExpr `("&" @@)?` } type UnionExpr struct { Left *IntersectionExpr `@@` Right *UnionExpr `("|" @@)?` } type ClassNameExpr struct { Part string `@Ident` Next *ClassNameExpr `("\\" @@)?` } Новое AST 👍 Старое AST 👎 39

Slide 40

Slide 40 text

// primary ::= type_name | '(' union_expr ')' func (conv *converter) convertPrimary(expr *PrimaryExpr) phpdoc.Type { if expr.TypeName != nil { return conv.convertTypeName(expr.TypeName) } if expr.Parens != nil { return conv.convertRoot(expr.Parens) } return nil } Пример конвертирования AST 40

Slide 41

Slide 41 text

// type_name ::= primitive_type_name | class_name func (conv *converter) convertTypeName(expr *TypeNameExpr) phpdoc.Type { if expr.Primitive != nil { return &phpdoc.PrimitiveTypeName{Name: *expr.Primitive} } if expr.Class != nil { return conv.convertClassName(expr.Class, nil) } return nil } Пример конвертирования AST 41

Slide 42

Slide 42 text

● Проще менять парсер ● Увеличивает удобство работы с AST во всём остальном коде ● Очередной IR ● Конвертировать AST не бесплатно - замедляем наш парсинг Плюсы (+) Минусы (-) 42

Slide 43

Slide 43 text

Тестируем парсер 43

Slide 44

Slide 44 text

tests := []struct { input string expect string }{ {`int`, `int`}, {`float`, `float`}, {`A\B\C`, `A\B\C`}, {`(int)`, `int`}, {`?int`, `?(int)`}, } Тестовые кейсы 44 Ожидаемый результат в формате строки, создаваемой Type.String()

Slide 45

Slide 45 text

typ, err := p.Parse(` ` + test.input + ` `) if err != nil { // fail: unexpected error } if typ.String() != test.expect { // fail: printed form mismatches } Парсим и сравниваем 45

Slide 46

Slide 46 text

typ2, err := p.Parse(typ.String()) if err != nil { // fail: unexpected error during re-parse } if typ.String() != typ2.String() { // fail: re-parse result mismatches } Парсим повторно и сравниваем 46

Slide 47

Slide 47 text

47 ФАЗЗИНГ (кродеться)

Slide 48

Slide 48 text

goyacc + text/scanner 48

Slide 49

Slide 49 text

1. Пишем файл грамматики (.y) 2. Генерируем парсер (через запуск goyacc) 3. Реализуем лексер под него 4. Соединяем лексер с парсером Используем yacc 49

Slide 50

Slide 50 text

// A simple lexer that uses a scanner.Scanner // to do the lexing. It also stores // the parse result inside itself. type yyLex struct { s scanner.Scanner result phpdoc.Type } Вводим заготовку для лексера 50

Slide 51

Slide 51 text

// A simple lexer that uses a scanner.Scanner // to do the lexing. It also stores // the parse result inside itself. type yyLex struct { s scanner.Scanner result phpdoc.Type } Вводим заготовку для лексера 51 yy?! дефолтный префикс для yacc

Slide 52

Slide 52 text

// превратится в структуру yySymType %union{ tok rune // id текущего токена text string // текст для T_NAME токенов expr phpdoc.Type // для правил с типами } phpdoc.y: определяем “symbol value” 52

Slide 53

Slide 53 text

example : T_NAME ':' type_expr { var text string = $1 var expr phpdoc.Type = $3 ... } Типы токенов и нетерминальных правил 53

Slide 54

Slide 54 text

%token T_NULL %token T_FALSE %token T_INT %token T_FLOAT %token T_STRING %token T_BOOL %token T_NAME phpdoc.y: декларация токенов 54 %union{ tok rune text string expr phpdoc.Type }

Slide 55

Slide 55 text

%token T_NULL %token T_FALSE %token T_INT %token T_FLOAT %token T_STRING %token T_BOOL %token T_NAME phpdoc.y: декларация токенов 55 %union{ tok rune text string expr phpdoc.Type }

Slide 56

Slide 56 text

%token T_NULL %token T_FALSE %token T_INT %token T_FLOAT %token T_STRING %token T_BOOL %token T_NAME phpdoc.y: декларация токенов 56 %union{ tok rune text string expr phpdoc.Type }

Slide 57

Slide 57 text

%token T_NULL %token T_FALSE %token T_INT %token T_FLOAT %token T_STRING %token T_BOOL %token T_NAME phpdoc.y: декларация токенов 57 Ассоциированные id токенов Для них будут созданы константы

Slide 58

Slide 58 text

%right '|' // самый низкий приоритет %right '&' %left OPTIONAL // постфиксный '?' %right '[' %left '?' // самый высокий приоритет phpdoc.y: приоритеты и ассоциативность 58

Slide 59

Slide 59 text

start : type_expr { yylex.(*yyLex).result = $1 } ; phpdoc.y: описываем синтаксис 59

Slide 60

Slide 60 text

start : type_expr { yylex.(*yyLex).result = $1 } ; phpdoc.y: описываем синтаксис 60 %type type_expr

Slide 61

Slide 61 text

start : type_expr { yylex.(*yyLex).result = $1 } ; phpdoc.y: описываем синтаксис 61 yyParser.Parse(lexer)

Slide 62

Slide 62 text

start : type_expr { yylex.(*yyLex).result = $1 } ; phpdoc.y: описываем синтаксис 62 type yyLex struct { s scanner.Scanner result phpdoc.Type }

Slide 63

Slide 63 text

primitive_type : T_NULL { $$ = &PrimitiveTypeName{Name: "null"} } | T_FALSE { $$ = &PrimitiveTypeName{Name: "false"} } | T_INT { $$ = &PrimitiveTypeName{Name: "int"} } | T_FLOAT { $$ = &PrimitiveTypeName{Name: "float"} } | T_STRING { $$ = &PrimitiveTypeName{Name: "string"} } | T_BOOL { $$ = &PrimitiveTypeName{Name: "bool"} } ; phpdoc.y: описываем синтаксис 63

Slide 64

Slide 64 text

type_expr : primitive_type { $$ = $1 } | T_NAME { $$ = &TypeName{Parts: strings.Split($1, `\`)} } | '(' type_expr ')' { $$ = $2 } | '?' type_expr { $$ = &NullableType{Elem: $2} } | type_expr '[' ']' { $$ = &ArrayType{Elem: $1} } | type_expr '?' %prec OPTIONAL { $$ = &OptionalKeyType{Elem: $1} } | type_expr '&' type_expr { $$ = &IntersectionType{X: $1, Y: $3} } | type_expr '|' type_expr { $$ = &UnionType{X: $1, Y: $3} } ; phpdoc.y: описываем синтаксис 64

Slide 65

Slide 65 text

Запускаем goyacc 65 phpdoc.y y.go goyacc грамматика Сгенерированный файл $ goyacc phpdoc.y

Slide 66

Slide 66 text

func (l *yyLex) Lex(sym *yySymType) int { tok := l.nextToken(sym) sym.tok = tok return int(tok) } Реализуем лексер 66 %union{ tok rune text string expr phpdoc.Type }

Slide 67

Slide 67 text

func (l *yyLex) nextToken(sym *yySymType) rune { tok := l.s.Scan() if tok == scanner.Ident { text := l.s.TokenText() if tok, ok := nameToToken[text]; ok { return tok } sym.text = text return T_NAME } return tok } Реализуем лексер 67

Slide 68

Slide 68 text

var nameToToken = map[string]rune{ "int": T_INT, "float": T_FLOAT, "null": T_NULL, "string": T_STRING, "false": T_FALSE, "bool": T_BOOL, } Реализуем лексер 68

Slide 69

Slide 69 text

69

Slide 70

Slide 70 text

input := "int|?float" lexer := NewLexer() lexer.s.Init(strings.NewReader(input)) parser := yyNewParser() parser.Parse(lexer) result := lexer.result Связываем воедино и запускаем! 70

Slide 71

Slide 71 text

● Генерирует эффективные парсеры ● Можно сразу собирать красивое AST ● Удобно описывать приоритеты и ассоциативность ● Требует стороннюю утилиту (goyacc) ● Не очень красивая интеграция с Go (но в целом ОК) Плюсы goyacc (+) Минусы goyacc (-) 71

Slide 72

Slide 72 text

В исходниках vk.com примерно 400000 phpdoc типов Отслеживаем производительность 72 Метод парсинга Парсим Foo|Bar|null 400000 раз participle 78000 наносек ~31 сек goyacc + text/scanner 4200 наносек ~1.68 сек

Slide 73

Slide 73 text

goyacc + ragel 73

Slide 74

Slide 74 text

// обычный код... %%{ Ragel сниппет (multi-line) %%} %% Ragel директива (single-line); // обычный код... Структура Ragel файлов 74

Slide 75

Slide 75 text

package main %%machine lexer; %%write data; type yyLex struct { pos int src string result phpdoc.Type } Реализуем лексер 75

Slide 76

Slide 76 text

package main %%machine lexer; %%write data; type yyLex struct { pos int src string result phpdoc.Type } Реализуем лексер 76 Название для генерируемой FSM (играет роль префикса)

Slide 77

Slide 77 text

package main %%machine lexer; %%write data; type yyLex struct { pos int src string result phpdoc.Type } Реализуем лексер 77 Вставляем в это место константы и таблицы для FSM

Slide 78

Slide 78 text

package main %%machine lexer; %%write data; type yyLex struct { pos int src string result phpdoc.Type } Реализуем лексер 78 Теперь мы сами будем сканировать входную строку

Slide 79

Slide 79 text

func (l *yyLex) Lex(lval *yySymType) int { tok := 0 // TODO: сюда вставим boilerplate %%{ // TODO: декларация FSM %%} %%write init; %%write exec; return tok } Реализуем лексер 79

Slide 80

Slide 80 text

data := l.src // string или []byte p := l.pos // текущее смещение (внутри data) pe := len(data) // позиция окончания eof := pe // позиция EOF // ts и te - это начало/конец токена var cs, ts, te, act int Boilerplate 80

Slide 81

Slide 81 text

whitespace = [\t ]; ident_first = [a-zA-Z_] | (0x0080..0x00FF); ident_rest = ident_first | [0-9] | [\\]; ident = ident_first (ident_rest)*; Описываем FSM 81

Slide 82

Slide 82 text

main := |* whitespace => {}; 'int' => { tok = T_INT; fbreak; }; 'float' => { tok = T_FLOAT; fbreak; }; 'null' => { tok = T_NULL; fbreak; }; 'string' => { tok = T_STRING; fbreak; }; 'false' => { tok = T_FALSE; fbreak; }; 'bool' => { tok = T_BOOL; fbreak; }; ident => { tok = T_NAME; fbreak; }; any => { tok = int(data[ts]); fbreak; }; *|; Описываем FSM 82

Slide 83

Slide 83 text

83 lexer.rl lexer.go Ragel файл Сгенерированный файл $ ragel -Z -G2 lexer.rl -o lexer.go

Slide 84

Slide 84 text

84 $ ragel -Z -G2 lexer.rl -o lexer.go Тип генерируемой FSM, G0, G1, G2 - goto-based Внимание: может сгенерировать десятки тысяч строк кода

Slide 85

Slide 85 text

● Создаёт нереально быстрый код (быстрее golex) ● Удобный в использовании (лучше golex) ● Полезен не только для лексеров/парсеров Почему именно Ragel, а не golex? 85 Рандомный факт: php-parser когда-то использовал golex вместо ragel

Slide 86

Slide 86 text

● Можно создать очень эффективный лексер под любой парсер ● Делает ваши волосы более шелковистыми ● Требует ещё одной сторонней утилиты (ragel) Плюсы ragel (+) Минусы ragel (-) 86

Slide 87

Slide 87 text

В исходниках vk.com примерно 400000 phpdoc типов Отслеживаем производительность 87 Метод парсинга Парсим Foo|Bar|null 400000 раз participle 78000 наносек ~31 сек goyacc + text/scanner 4200 наносек ~1.68 сек goyacc + ragel 1600 наносек ~0.6 сек

Slide 88

Slide 88 text

Пишем парсер ручками 88

Slide 89

Slide 89 text

Некоторые из сигналов: ● Для вашего формата нет хорошей грамматики ● Написать грамматику для формата слишком сложно ● Нужно разбирать частично некорректные данные ● Вы feeling lucky ¯\_(ツ)_/¯ Зачем вообще писать парсер руками? 89

Slide 90

Slide 90 text

Рекурсивный спуск для людей 90 Парсеры Пратта

Slide 91

Slide 91 text

Статьи с примерами на Go: ● Парсеры Пратта в Go (ru) ● Pratt parsers in Go (en) Оригинал с примерами на Java: ● Pratt Parsers: Expression Parsing Made Easy Введение в парсеры Пратта 91

Slide 92

Slide 92 text

В NoVerify* парсер phpdoc типов у нас написан руками 92 (*) Линтер для PHP, написанный на Go

Slide 93

Slide 93 text

● Умеет разбирать частично некорректные типы ● Во многих случаях не делает аллокаций ● Поддерживает сложные типы вроде генериков Sources: noverify/src/phpdoc/type_parser.go Парсер phpdoc типов NoVeirfy 93

Slide 94

Slide 94 text

В исходниках vk.com примерно 400000 phpdoc типов Отслеживаем производительность 94 Метод парсинга Парсим Foo|Bar|null 400000 раз participle 78000 наносек ~31 сек goyacc + text/scanner 4200 наносек ~1.68 сек goyacc + ragel 1600 наносек ~0.6 сек ручной парсер 800 наносек ~0.3 сек

Slide 95

Slide 95 text

В исходниках vk.com примерно 400000 phpdoc типов Отслеживаем производительность 95 Метод парсинга Парсим Foo|Bar|null 400000 раз participle 78000 наносек ~31 сек goyacc + text/scanner 4200 наносек ~1.68 сек goyacc + ragel 1600 наносек ~0.6 сек ручной парсер 800 наносек ~0.3 сек ручной парсер (no conv) 250 наносек ~0.1 сек

Slide 96

Slide 96 text

Почти всё 96

Slide 97

Slide 97 text

github.com/quasilyte/parsing-and-go ● Парсер на основе participle ● goyacc+text/scanner ● goyacc+ragel ● Вручную написанный парсер ● Тесты ● Бенчмарки Исходники всех примеров (готовые парсеры) 97

Slide 98

Slide 98 text

● Обработка ошибок в парсере и лексере ● Проставление локаций/позиций для AST элементов ● participle с Ragel лексером и альтернативной грамматикой ● Пулы объектов для парсеров ● Некоторые другие либы, типа pigeon (аналог participle) ● Как ещё тестировать парсеры (тесты позиций) ● Что делать с комментариями (freefloating токены) ● Левая рекурсия и прочие заболевания ... Что мы не разобрали (домашнее задание) 98

Slide 99

Slide 99 text

Работа с грамматиками: ● Resolving common grammar conflicts in parsers ● Crafting interpreters: parsing expressions Полезные штучки 99

Slide 100

Slide 100 text

Ragel: ● Ragel: state machine compiler ● Lexing with Ragel Parsing with Yacc ● Speeding up regexp matching with ragel ● Ragel cheat sheet Полезные штучки 100

Slide 101

Slide 101 text

Парсеры Пратта: ● Pratt Parsers: Expression Parsing Made Easy ● Pratt parsers in Go (ru) ● Pratt parsers in Go (en) Полезные штучки 101

Slide 102

Slide 102 text

@quasilyte 2021 Parsing & Go 102