Статьи

Начало работы с ANTLR: создание простого языка выражений

Этот пост является первым из серии. Цель серии — описать, как создать полезный язык и все вспомогательные инструменты.

В этом посте мы начнем работать над очень простым языком выражений. Мы создадим его в нашей языковой песочнице и поэтому будем называть язык Sandy .

Я думаю, что поддержка инструментов жизненно важна для языка: по этой причине мы начнем с очень простого языка, но мы построим для него богатую инструментальную поддержку. Чтобы извлечь пользу из языка, нам нужны парсер, интерпретаторы и компиляторы, редакторы и многое другое. Мне кажется, что есть много материала по созданию простых парсеров, но очень мало материала по созданию остальной инфраструктуры, необходимой для того, чтобы сделать использование языка практичным и эффективным .

Я хотел бы сосредоточиться именно на этих аспектах, сделав язык небольшим, но полностью полезным. Тогда вы сможете органично развивать свой язык.

Код доступен на GitHub: https://github.com/ftomassetti/LangSandbox . Код, представленный в этой статье, соответствует тегу 01_lexer.

Язык

Язык позволит определять переменные и выражения. Мы будем поддерживать:

  • целые и десятичные литералы
  • определение и присвоение переменной
  • основные математические операции (сложение, вычитание, умножение, деление)
  • использование скобок

Примеры правильного файла:

1
2
3
var a = 10 / 3
var b = (5 + 3) * 2
var 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 generateGrammarSource
sourceSets {
    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;
  
// Whitespace
NEWLINE            : '\r\n' | 'r' | '\n' ;
WS                 : [\t ]+ ;
  
// Keywords
VAR                : 'var' ;
  
// Literals
INTLIT             : '0'|[1-9][0-9]* ;
DECLIT             : '0'|[1-9][0-9]* '.' [0-9]+ ;
  
// Operators
PLUS               : '+' ;
MINUS              : '-' ;
ASTERISK           : '*' ;
DIVISION           : '/' ;
ASSIGN             : '=' ;
LPAREN             : '(' ;
RPAREN             : ')' ;
  
// Identifiers
ID                 : [_]*[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.SandyLexer
import org.antlr.v4.runtime.ANTLRInputStream
import java.io.*
import java.util.*
import org.junit.Test as test
import 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")))
    }
}

Выводы и дальнейшие шаги

Мы начали с первого маленького шага: мы настроили проект и создали лексер.

Нам предстоит пройти долгий путь, прежде чем мы сможем использовать язык на практике, но мы начали. Далее мы будем работать с парсером с тем же подходом: создадим что-то простое, что мы можем протестировать и скомпилировать с помощью командной строки.