Этот пост является первым из серии. Цель серии — описать, как создать полезный язык и все вспомогательные инструменты.
В этом посте мы начнем работать над очень простым языком выражений. Мы создадим его в нашей языковой песочнице и поэтому будем называть язык Sandy .
Я думаю, что поддержка инструментов жизненно важна для языка: по этой причине мы начнем с очень простого языка, но мы построим для него богатую инструментальную поддержку. Чтобы извлечь пользу из языка, нам нужны парсер, интерпретаторы и компиляторы, редакторы и многое другое. Мне кажется, что есть много материала по созданию простых парсеров, но очень мало материала по созданию остальной инфраструктуры, необходимой для того, чтобы сделать использование языка практичным и эффективным .
Я хотел бы сосредоточиться именно на этих аспектах, сделав язык небольшим, но полностью полезным. Тогда вы сможете органично развивать свой язык.
Код доступен на GitHub: https://github.com/ftomassetti/LangSandbox . Код, представленный в этой статье, соответствует тегу 01_lexer.
Язык
Язык позволит определять переменные и выражения. Мы будем поддерживать:
- целые и десятичные литералы
- определение и присвоение переменной
- основные математические операции (сложение, вычитание, умножение, деление)
- использование скобок
Примеры правильного файла:
|
1
2
3
|
var a = 10 / 3var b = (5 + 3) * 2var c = a / b |
Инструменты, которые мы будем использовать
Мы будем использовать:
- ANTLR для генерации лексера и парсера
- использовать Gradle в качестве нашей системы сборки
- напиши код в котлине. Это будет очень простой Kotlin, учитывая, что я только начал изучать его.
Настройте проект
Наша сборка. Gradle файл будет выглядеть так
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
buildscript { ext.kotlin_version = '1.0.3' repositories { mavenCentral() maven { name 'JFrog OSS snapshot repo' } jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" }} apply plugin: 'kotlin'apply plugin: 'java'apply plugin: 'idea'apply plugin: 'antlr' repositories { mavenLocal() mavenCentral() jcenter()} dependencies { antlr "org.antlr:antlr4:4.5.1" compile "org.antlr:antlr4-runtime:4.5.1" compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" testCompile 'junit:junit:4.12'} generateGrammarSource { maxHeapSize = "64m" arguments += ['-package', 'me.tomassetti.langsandbox'] outputDirectory = new File("generated-src/antlr/main/me/tomassetti/langsandbox".toString())}compileJava.dependsOn generateGrammarSourcesourceSets { generated { java.srcDir 'generated-src/antlr/main/' }}compileJava.source sourceSets.generated.java, sourceSets.main.java clean{ delete "generated-src"} idea { module { sourceDirs += file("generated-src/antlr/main") }} |
Мы можем запустить:
- ./gradlew идея для создания файлов проекта IDEA
- ./gradlew generateGrammarSource для генерации лексера и синтаксического анализатора ANTLR
Реализация лексера
Мы будем строить лексер и парсер в двух отдельных файлах. Это лексер:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
lexer grammar SandyLexer; // WhitespaceNEWLINE : '\r\n' | 'r' | '\n' ;WS : [\t ]+ ; // KeywordsVAR : 'var' ; // LiteralsINTLIT : '0'|[1-9][0-9]* ;DECLIT : '0'|[1-9][0-9]* '.' [0-9]+ ; // OperatorsPLUS : '+' ;MINUS : '-' ;ASTERISK : '*' ;DIVISION : '/' ;ASSIGN : '=' ;LPAREN : '(' ;RPAREN : ')' ; // IdentifiersID : [_]*[a-z][A-Za-z0-9_]* ; |
Теперь мы можем просто запустить ./ gradlew generateGrammarSource и лексер будет сгенерирован для нас из предыдущего определения.
Тестирование лексера
Тестирование всегда важно, но при создании языков это абсолютно необходимо: если инструменты, поддерживающие ваш язык, не верны, это может повлиять на все возможные программы, которые вы создадите для них. Итак, давайте начнем тестировать лексер: мы просто проверим, что последовательность токенов, которые производит лексер, является той, которую мы аспектируем.
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
package me.tomassetti.sandy import me.tomassetti.langsandbox.SandyLexerimport org.antlr.v4.runtime.ANTLRInputStreamimport java.io.*import java.util.*import org.junit.Test as testimport kotlin.test.* class SandyLexerTest { fun lexerForCode(code: String) = SandyLexer(ANTLRInputStream(StringReader(code))) fun lexerForResource(resourceName: String) = SandyLexer(ANTLRInputStream(this.javaClass.getResourceAsStream("/${resourceName}.sandy"))) fun tokens(lexer: SandyLexer): List<String> { val tokens = LinkedList<String>() do { val t = lexer.nextToken() when (t.type) { -1 -> tokens.add("EOF") else -> if (t.type != SandyLexer.WS) tokens.add(lexer.ruleNames[t.type - 1]) } } while (t.type != -1) return tokens } @test fun parseVarDeclarationAssignedAnIntegerLiteral() { assertEquals(listOf("VAR", "ID", "ASSIGN", "INTLIT", "EOF"), tokens(lexerForCode("var a = 1"))) } @test fun parseVarDeclarationAssignedADecimalLiteral() { assertEquals(listOf("VAR", "ID", "ASSIGN", "DECLIT", "EOF"), tokens(lexerForCode("var a = 1.23"))) } @test fun parseVarDeclarationAssignedASum() { assertEquals(listOf("VAR", "ID", "ASSIGN", "INTLIT", "PLUS", "INTLIT", "EOF"), tokens(lexerForCode("var a = 1 + 2"))) } @test fun parseMathematicalExpression() { assertEquals(listOf("INTLIT", "PLUS", "ID", "ASTERISK", "INTLIT", "DIVISION", "INTLIT", "MINUS", "INTLIT", "EOF"), tokens(lexerForCode("1 + a * 3 / 4 - 5"))) } @test fun parseMathematicalExpressionWithParenthesis() { assertEquals(listOf("INTLIT", "PLUS", "LPAREN", "ID", "ASTERISK", "INTLIT", "RPAREN", "MINUS", "DECLIT", "EOF"), tokens(lexerForCode("1 + (a * 3) - 5.12"))) }} |
Выводы и дальнейшие шаги
Мы начали с первого маленького шага: мы настроили проект и создали лексер.
Нам предстоит пройти долгий путь, прежде чем мы сможем использовать язык на практике, но мы начали. Далее мы будем работать с парсером с тем же подходом: создадим что-то простое, что мы можем протестировать и скомпилировать с помощью командной строки.
| Ссылка: | Начало работы с ANTLR: создание простого языка выражений от нашего партнера по JCG |