MyLang — это простой образовательный язык программирования, вдохновленный Python
, JavaScript
и C
, написанный как личный вызов за короткое время, в основном для того, чтобы развлечься написанием парсера с рекурсивным спуском и изучить мир интерпретаторов. Не ждите полноценный язык сценариев с библиотеками и платформами, готовыми к использованию в производстве. Однако MyLang
имеет минимальный набор встроенных функций и его можно использовать и в практических целях.
MyLang написан на портативном C++17: на данный момент у проекта нет никаких зависимостей, кроме стандартной библиотеки C++. Чтобы собрать его, если у вас установлен GNU make
, просто запустите:
$ make -j
В противном случае просто передайте все файлы .cpp своему компилятору и добавьте каталог src/
в путь поиска включения. Одна из самых приятных особенностей отсутствия зависимостей заключается в том, что для одноразовых сборок нет необходимости в системе сборки .
Просто передайте опцию BUILD_DIR
, чтобы make
:
$ make -j BUILD_DIR=other_build_directory
Если вы также хотите запустить тесты MyLang, вам нужно просто скомпилировать с TESTS=1 и отключить оптимизацию с OPT=0, чтобы улучшить качество отладки:
$ make -j TESTS=1 OPT=0
Затем запустите все тесты с помощью:
$ ./build/mylang -rt
Стоит отметить, что хотя тестовые платформы, такие как GoogleTest и Boost.Test, гораздо более мощные и гибкие, чем тривиальный тестовый движок, который у нас есть в src/tests.cpp
, они являются внешними зависимостями. Чем меньше зависимостей, тем лучше, верно? :-)
Самый короткий способ описать MyLang
: это динамический язык Python в стиле C. Вероятно, самый быстрый способ выучить этот язык — это просмотреть скрипты в каталоге samples/
и просмотреть короткую документацию ниже.
MyLang
— это динамический язык утиной печати, такой как Python
. Если вы знаете Python
и готовы использовать фигурные скобки { }
, вы сможете их использовать автоматически. Никаких сюрпризов. Строки неизменяемы, как в Python
, массивы могут быть определены с помощью [ ]
как в Python
, а словари также могут быть определены с помощью { }
. Язык также поддерживает фрагменты массива, используя тот же синтаксис [start:end]
который используется в Python
.
Сказал, что MyLang
отличается от Python
и других языков сценариев в нескольких аспектах:
Имеется поддержка констант времени анализа , объявленных с помощью const
.
Все переменные должны быть объявлены с помощью var
.
Переменные имеют область видимости, как в C
Тень поддерживается, когда переменная явно переобъявляется с помощью var
во вложенном блоке.
Все выражения-выражения должны заканчиваться ;
как в C
, C++
и Java
.
Ключевые слова true
и false
существуют, но нет boolean
типа. Как и в C, 0
— ложь, все остальное — true
. Однако строки, массивы и словари имеют логическое значение, точно так же, как в Python
(например, пустой массив считается false
). true
встроенная функция — это просто псевдоним целого числа 1
.
Оператор присваивания =
можно использовать, как и в C
, внутри выражений, но такого понятия, как оператор запятая, не существует из-за возможности расширения массива.
MyLang поддерживает как классический цикл for
так и явный цикл foreach
.
MyLang на данный момент не поддерживает пользовательские типы. Однако словари поддерживают приятный синтаксический сахар: в дополнение к основному синтаксису d["key"]
для строковых ключей также поддерживается синтаксис d.key
.
Переменные всегда объявляются с помощью var
и находятся в той области, в которой они были объявлены (при этом они видимы во вложенных областях). Например:
# Variable declared in the global scope
var a = 42 ;
{
var b = 12 ;
# Here we can see both `a` and `b`
print ( a , b ) ;
}
# But here we cannot see `b` .
Можно объявить несколько переменных, используя следующий знакомый синтаксис:
var a , b , c ;
Но есть одна оговорка, возможно, единственная «неожиданная» особенность MyLang
: инициализация переменных не работает так, как в C. Рассмотрим следующее утверждение:
var a , b , c = 42 ;
В этом случае вместо того, чтобы просто объявлять a
и b
и инициализировать c
значением 42, мы инициализируем все три переменные значением 42. Чтобы инициализировать каждую переменную различным значением, используйте синтаксис расширения массива:
var a , b , c = [ 1 , 2 , 3 ] ;
Константы объявляются так же, как и переменные , но их нельзя скрыть во вложенных областях. Например:
const c = 42 ;
{
# That's not allowed
const c = 1 ;
# That's not allowed as well
var c = 99 ;
}
В MyLang
константы оцениваются во время анализа , аналогично объявлениям constexpr в C++
(но здесь мы говорим о времени компиляции ). При инициализации const
в дополнение ко всему набору встроенных констант можно использовать любой литерал. Например:
const val = sum ( [ 1 , 2 , 3 ] ) ;
const x = " hello " + " world " + " " + join ( [ " a " , " b " , " c " ] , " , " ) ;
Чтобы понять, как именно была вычислена константа, запустите интерпретатор с опцией -s
, чтобы выгрузить дерево абстрактного синтаксиса перед запуском сценария. Для примера выше:
$ cat > t
const val = sum([1,2,3]);
const x = "hello" + " world" + " " + join(["a","b","c"], ",");
$ ./build/mylang t
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
)
--------------------------
Удивлен? Ну, константы, кроме массивов и словарей, даже не создаются как переменные. Их просто не существует во время выполнения . Давайте добавим оператор, используя x
:
$ cat >> t
print(x);
$ cat t
const val = sum([1,2,3]);
const x = "hello" + " world" + " " + join(["a","b","c"], ",");
print(x);
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
CallExpr(
Id("print")
ExprList(
"hello world a,b,c"
)
)
)
--------------------------
hello world a,b,c
Теперь все должно иметь смысл. Почти то же самое происходит с массивами и словарями, за исключением того, что последние также создаются во время выполнения, чтобы избежать повсюду потенциально огромных литералов. Рассмотрим следующий пример:
$ ./build/mylang -s -e 'const ar=range(4); const s=ar[2:]; print(ar, s, s[0]);'
Syntax tree
--------------------------
Block(
ConstDecl(
Id("ar")
Op '='
LiteralArray(
Int(0)
Int(1)
Int(2)
Int(3)
)
)
ConstDecl(
Id("s")
Op '='
LiteralArray(
Int(2)
Int(3)
)
)
CallExpr(
Id("print")
ExprList(
Id("ar")
Id("s")
Int(2)
)
)
)
--------------------------
[0, 1, 2, 3] [2, 3] 2
Как видите, операция среза оценивалась во время анализа при инициализации константы s
, но оба массива существуют и во время выполнения. Вместо этого операции с индексами в константных выражениях преобразуются в литералы. Это выглядит как хороший компромисс для производительности: небольшие значения, такие как целые числа, числа с плавающей запятой и строки, преобразуются в литералы во время вычисления константы , в то время как массивы и словари (потенциально большие) остаются символами только для чтения во время выполнения, но все же позволяют некоторые операции над ними (например, [index]
и len(arr)
) должны оцениваться константно.
MyLang
на данный момент поддерживает только следующие (встроенные) типы:
None Тип none
, эквивалент None
в Python. Переменные, только что объявленные без присвоенного им значения, none
имеют значения (например, var x;
). То же самое относится и к функциям, которые не имеют возвращаемого значения. Кроме того, оно используется в качестве специального значения встроенными функциями, такими как find()
, в случае неудачи.
Integer Целое число со знаком размера указателя (например, 3
).
Float Число с плавающей запятой (например, 1.23
). Внутри это длинный дубль.
Строка Строка типа «привет». Строки являются неизменяемыми и поддерживают срезы (например, s[3:5]
или s[3:]
или s[-2:]
, имеющие то же значение, что и в Python
.
Array Изменяемый тип для массивов и кортежей (например [1,2,3]
). Он может содержать элементы разных типов и поддерживает записываемые фрагменты. Срезы массива ведут себя как копии, хотя внутри они используют методы копирования при записи.
Словарь Словари представляют собой хэш-карты, определенные с использованием синтаксиса Python
: {"a": 3, "b": 4}
. Доступ к элементам осуществляется с помощью знакомого синтаксиса d["key-string"]
или d[23]
, их можно найти с помощью find()
и удалить с помощью erase()
. На данный момент в качестве ключей словаря можно использовать только строки, целые числа и числа с плавающей запятой. Перки : к строковым ключам, подобным идентификаторам, можно получить доступ также с помощью синтаксиса «член»: d.key
.
Функция Как автономные функции, так и лямбда-выражения имеют один и тот же тип объекта и могут передаваться как любой другой объект (см. ниже). Но только лямбды могут иметь список захвата. Обычные функции не могут выполняться во время константной оценки, тогда как pure
функции могут. Чистые функции могут видеть только константы и их аргументы.
Exception Единственный тип объектов, который можно выбросить. Чтобы создать их, используйте встроенную exception()
или ее ярлык ex()
.
Условные операторы работают точно так же, как в C
Синтаксис:
if ( conditionExpr ) {
# Then block
} else {
# Else block
}
А фигурные скобки { }
можно опустить, как в C
, в случае блоков с одним оператором. conditionExpr
может быть любым выражением, например: (a=3)+b >= c && !d
.
Если conditionExpr
является выражением, которое может быть вычислено константно, весь оператор if заменяется истинной ветвью, а ложная ветвь отбрасывается. Например, рассмотрим следующий сценарий:
const a = 3 ;
const b = 4 ;
if ( a < b ) {
print ( " yes " ) ;
} else {
print ( " no " ) ;
}
Он не только всегда печатает «да», но ему даже не нужно ничего проверять перед этим. Проверьте абстрактное синтаксическое дерево:
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
Block(
CallExpr(
Id("print")
ExprList(
"yes"
)
)
)
)
--------------------------
yes
MyLang
поддерживает классические циклы while
и for
.
while ( condition ) {
# body
if ( something )
break ;
if ( something_else )
continue ;
}
for ( var i = 0 ; i < 10 ; i += 1 ) {
# body
if ( something )
break ;
if ( something_else )
continue ;
}
Здесь фигурные скобки { }
можно опустить, как и в приведенном выше случае. Есть лишь несколько отличий от C
на которые стоит обратить внимание:
Операторы ++
и --
на данный момент не существуют в MyLang
.
Чтобы объявить несколько переменных, используйте синтаксис: var a, b = [3,4];
или просто var a,b,c,d = 0;
если вы хотите, чтобы все переменные имели одинаковое начальное значение.
Чтобы увеличить значение нескольких переменных, используйте синтаксис: a, b += [1, 2]
. В чрезвычайно редких и сложных случаях, когда в инструкции приращения цикла for нам нужно присвоить каждой переменной новую переменную, используя разные выражения, воспользуйтесь синтаксисом расширения при присваивании: i, j = [i+2, my_next(i, j*3)]
.
MyLang
поддерживает циклы foreach
используя довольно знакомый синтаксис:
var arr = [ 1 , 2 , 3 ] ;
foreach ( var e in arr ) {
print ( " elem: " , e ) ;
}
Циклы foreach можно использовать для массивов, строк и словарей. Например, перебирать каждую пару <key, value>
в словаре легко:
var d = { " a " : 3 , " b " : 10 , " c " : 42 } ;
foreach ( var k , v in d ) {
print ( k + " => " + str ( v ) ) ;
}
Чтобы перебирать только каждый ключ, просто используйте вместо этого var k in d
.
MyLang
также поддерживает перечисление в циклах foreach. Проверьте следующий пример:
var arr = [ " a " , " b " , " c " ] ;
foreach ( var i , elem in indexed arr ) {
print ( " elem[ " + str ( i ) + " ] = " + elem ) ;
}
Другими словами, когда имени контейнера предшествует ключевое слово indexed
, первой переменной присваивается прогрессивный номер на каждой итерации.
При переборе массива небольших массивов фиксированного размера (подумайте о кортежах) можно напрямую расширить эти «кортежи» в цикле foreach:
var arr = [
[ " hello " , 42 ] ,
[ " world " , 11 ]
] ;
foreach ( var name , value in arr ) {
print ( name , value ) ;
}
# This is a shortcut for :
foreach ( var elem in arr ) {
# regular array expansion
var name , value = elem ;
print ( name , value ) ;
}
# Which is a shortcut for :
foreach ( var elem in arr ) {
var name = elem [ 0 ] ;
var value = elem [ 1 ] ;
print ( name , value ) ;
}
Объявить функцию просто:
func add ( x , y ) {
return x + y ;
}
Но также поддерживаются несколько ярлыков. Например, в случае функций с одним оператором, подобных приведенной выше, можно использовать следующий синтаксис:
func add ( x , y ) => x + y ;
Кроме того, хотя для функций без параметров рекомендуется всегда писать ()
, на самом деле в этом языке они не являются обязательными:
func do_something { print ( " hello " ) ; }
В MyLang
функции рассматриваются как обычные символы, и в этом языке нет существенных различий между автономными функциями и лямбда-выражениями. Например, мы можем объявить функцию add
(выше) как лямбду следующим образом:
var add = func ( x , y ) => x + y ;
Примечание. При создании объектов-функций в выражениях нам не разрешено присваивать им имена.
Lambdas также поддерживает список захвата, но неявный захват не поддерживается, чтобы обеспечить ясность. Конечно, лямбда-выражения могут быть возвращены как любой другой объект. Например:
func create_adder_func ( val ) =>
func [ val ] ( x ) => x + val ;
var f = create_adder_func ( 5 ) ;
print ( f ( 1 ) ) ; # Will print 6
print ( f ( 10 ) ) ; # Will print 15
Лямбды с захватами имеют состояние , как и следовало ожидать. Рассмотрим следующий сценарий:
func gen_counter ( val ) => func [ val ] {
val += 1 ;
return val ;
} ;
var c1 = gen_counter ( 5 ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c1: " , c1 ( ) ) ;
# Clone the `c1` lambda object as `c2` : now it will have
# its own state , indipendent from `c1` .
var c2 = clone ( c1 ) ;
print ( ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c2: " , c2 ( ) ) ;
print ( ) ;
for ( var i = 0 ; i < 3 ; i += 1 )
print ( " c1: " , c1 ( ) ) ;
Он генерирует вывод:
c1: 6
c1: 7
c1: 8
c2: 9
c2: 10
c2: 11
c1: 9
c1: 10
c1: 11
Обычные пользовательские функциональные объекты (включая лямбда-выражения) не считаются const
и, следовательно, не могут быть запущены во время константной оценки. Это довольно сильное ограничение. Рассмотрим следующий пример:
const people = [
[ " jack " , 3 ] ,
[ " alice " , 11 ] ,
[ " mario " , 42 ] ,
[ " bob " , 38 ]
] ;
const sorted_people = sort ( people , func ( a , y ) => a [ 0 ] < b [ 0 ] ) ;
В этом случае скрипт не может создать константный массив sorted_people
. Поскольку мы передали объект функции встроенной функции const sort()
, мы получим ошибку ExpressionIsNotConstEx
. Конечно, если sorted_people
было объявлено как var
, скрипт запустится, но массив больше не будет константным, и мы не сможем извлечь выгоду из какой-либо оптимизации времени анализа . Таким образом, хотя встроенную функцию sort()
можно вызвать во время константной оценки, если она имеет собственный параметр compare func
, это больше невозможно.
Чтобы преодолеть только что описанное ограничение, MyLang
имеет специальный синтаксис для чистых функций. Когда функция объявлена с pure
ключевым словом, предшествующим func
, интерпретатор обрабатывает ее особым образом: ее можно вызывать в любое время, как во время константной оценки, так и во время выполнения , но функция не может видеть глобальные переменные или захватывать что-либо: она может только использовать константы и значения их параметров: это именно то, что нам нужно при вычислении константы. Например, чтобы сгенерировать sorted_people
во время константной оценки, достаточно написать:
const sorted_people = sort ( people , pure func ( a , b ) => a [ 0 ] < b [ 0 ] ) ;
Чистые функции могут быть определены как автономные функции, а также могут использоваться с неконстантными параметрами. Следовательно, если функцию можно объявить как pure
, ее всегда следует объявлять именно так. Например, рассмотрим следующий сценарий:
pure func add2 ( x ) => x + 2 ;
var non_const = 25 ;
print ( add2 ( non_const ) ) ;
print ( add2 ( 5 ) ) ;
Абстрактное синтаксическое дерево, которое движок языка будет использовать во время выполнения, будет следующим:
$ ./build/mylang -s t
Syntax tree
--------------------------
Block(
FuncDeclStmt(
Id("add2")
<NoCaptures>
IdList(
Id("x")
)
Expr04(
Id("x")
Op '+'
Int(2)
)
)
VarDecl(
Id("non_const")
Op '='
Int(25)
)
CallExpr(
Id("print")
ExprList(
CallExpr(
Id("add2")
ExprList(
Id("non_const")
)
)
)
)
CallExpr(
Id("print")
ExprList(
Int(7)
)
)
)
--------------------------
27
7
Как вы можете видеть, в первом случае фактический вызов функции происходит потому, что non_const
не является константой, а во втором случае это происходит так, как если бы мы передали буквальное целое число в print()
.
Как и другие конструкции, MyLang
имеет обработку исключений, аналогичную Python
, но использует синтаксис, аналогичный C++
. Базовой конструкцией является оператор try-catch
. Давайте посмотрим пример:
try {
var input_str = " blah " ;
var a = int ( input_str ) ;
} catch ( TypeErrorEx ) {
print ( " Cannot convert the string to integer " ) ;
}
Примечание. Если исключение генерируется постоянными выражениями (например, int("blah")
), во время константной оценки об ошибке будет сообщено напрямую, минуя любую логику обработки исключений. Причина этого заключается в обеспечении раннего отказа .
Также разрешено несколько операторов catch
:
try {
# body
} catch ( TypeErrorEx ) {
# error handling
} catch ( DivisionByZeroEx ) {
# error handling
}
А если с помощью одного и того же кода можно обработать несколько исключений, можно использовать и более короткий синтаксис:
try {
# body
} catch ( TypeErrorEx , DivisionByZeroEx as e ) {
# error handling
print ( e ) ;
} catch ( OutOfBoundsEx ) {
# error handling
}
Исключения могут содержать данные, но ни одно из встроенных исключений в настоящее время их не содержит. Список встроенных исключений времени выполнения , которые можно перехватить с помощью блоков try-catch
:
Вместо этого другие исключения, такие как SyntaxErrorEx
, не могут быть перехвачены. В MyLang
также возможно перехватить ЛЮБОЕ исключение, используя блок перехвата чего угодно:
try {
# body
} catch {
# Something went wrong .
}
На данный момент этот язык не поддерживает пользовательские типы. Поэтому невозможно выбросить какой-либо объект, как в некоторых других языках. Чтобы сгенерировать исключение, необходимо использовать специальную встроенную exception()
или ее ярлык ex()
. Рассмотрим следующий пример:
try {
throw ex ( " MyError " , 1234 ) ;
} catch ( MyError as e ) {
print ( " Got MyError, data: " , exdata ( e ) ) ;
}
Как подсказывает интуиция, с помощью ex()
мы создали, а затем инициировали объект исключения MyError
с номером 1234
в качестве полезных данных. Позже в блоке catch
мы перехватили исключение и извлекли полезные данные с помощью встроенной функции exdata()
.
В случае, если данному исключению не требуется полезная нагрузка, можно просто сохранить результат ex()
в переменной и выбросить его позже, используя, вероятно, более приятный синтаксис:
var MyError = ex ( " MyError " ) ;
throw MyError ;
MyLang
поддерживает повторное создание исключения в теле операторов catch с использованием специального ключевого слова rethrow
:
try {
do_something ( ) ;
} catch {
print ( " Something went wrong!! " ) ;
rethrow ;
}
В некоторых случаях может потребоваться выполнить некоторую очистку после выполнения блока кода, который может вызвать исключение. В этих случаях MyLang
поддерживает хорошо известное предложение finally
, которое работает точно так же, как в C#
:
try {
step1_might_throw ( ) ;
step2_might_throw ( ) ;
step3_might_throw ( ) ;
step4_might_throw ( ) ;
} catch ( TypeErrorEx ) {
# some error handling
} finally {
# clean - up
}
Стоит отметить, что конструкции try-finally
(без какого-либо предложения catch
) также разрешены.
Следующие встроенные функции будут оцениваться во время анализа , когда им передаются константные аргументы.
defined(symbol)
Проверьте, определен ли symbol
. Возвращает 1, если символ определен, и 0 в противном случае.
len(container)
Возвращает количество элементов в данном контейнере.
str(value, [decimal_digits])
Преобразуйте данное значение в строку. Если value
является числом с плавающей запятой, второй параметр указывает желаемое количество десятичных цифр в выходной строке.
int(value)
Преобразуйте данную строку в целое число. Если значение является плавающим, оно будет сокращено. Если значение является строкой, оно будет проанализировано и преобразовано в целое число, если это возможно. Если значение уже является целым числом, оно будет возвращено как есть.
float(value)
Преобразуйте данное значение в число с плавающей запятой. Если значение является целым числом, оно будет преобразовано в число с плавающей запятой. Если значение является строкой, оно будет проанализировано и преобразовано в число с плавающей запятой, если это возможно. Если значение уже является числом с плавающей запятой, оно будет возвращено как есть.
clone(obj)
Клонировать данный объект. Полезно для нетривиальных объектов, таких как массивы, словари и лямбда-выражения с захватами.
type(value)
Возвращает имя типа данного значения в строковой форме. Полезно для отладки.
hash(value)
Возвращает значение хеш-функции, используемое словарями внутри, когда value
используется в качестве ключа. На данный момент hash()
поддерживают только целые числа, числа с плавающей запятой и строки.
array(N)
Вернуть массив none